From 19c5e3e4c8f9739e94dadf24736a2d346dfb8c82 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sat, 26 Jun 2021 15:04:57 -0400 Subject: [PATCH] Consolidate item views. Add watched badges Add remaining episode badges Add favorite badges. Fix genres to only show genres from current library. Show watched episodes in series view. Add progress bar to currently watching items in library. Fix showing favorites. --- JellyfinPlayer.xcodeproj/project.pbxproj | 32 +++++++---- JellyfinPlayer/LibraryFilterView.swift | 6 ++- JellyfinPlayer/LibraryListView.swift | 54 ++++++++++--------- JellyfinPlayer/LibrarySearchView.swift | 31 +---------- JellyfinPlayer/LibraryView.swift | 44 +++------------ JellyfinPlayer/NextUpView.swift | 19 +------ JellyfinPlayer/SeasonItemView.swift | 19 ++++++- JellyfinPlayer/SeriesItemView.swift | 20 +------ Shared/Extensions/APIExtensions.swift | 2 +- .../ViewModels/LibraryFilterViewModel.swift | 7 ++- Shared/ViewModels/LibraryViewModel.swift | 4 +- Shared/ViewModels/SeriesItemViewModel.swift | 2 +- 12 files changed, 92 insertions(+), 148 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index a1ad7550..0063ed49 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53EC6E24267EB10F006DD26A /* SwiftyJSON */; }; 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; }; + 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; @@ -285,6 +286,7 @@ 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = ""; }; 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = ""; }; + 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemView.swift; sourceTree = ""; }; 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; @@ -404,23 +406,23 @@ 532175392671BCED005491E6 /* ViewModels */ = { isa = PBXGroup; children = ( - 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, - 625CB5692678B71200530A6E /* SplashViewModel.swift */, - 625CB5722678C32A00530A6E /* HomeViewModel.swift */, - 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, - 625CB57B2678CE1000530A6E /* ViewModel.swift */, - 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, + 62E632F2267D54030063E547 /* DetailItemViewModel.swift */, + 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, + 625CB5722678C32A00530A6E /* HomeViewModel.swift */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, + 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, + 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, + 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */, 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */, - 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */, 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, - 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, - 62E632F2267D54030063E547 /* DetailItemViewModel.swift */, + 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, + 625CB5692678B71200530A6E /* SplashViewModel.swift */, 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, + 625CB57B2678CE1000530A6E /* ViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -494,10 +496,10 @@ 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, + C78797A232E2B8774099D1E9 /* Pods */, 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, 628B95252670CABD0091AF3B /* WidgetExtension */, - C78797A232E2B8774099D1E9 /* Pods */, ); sourceTree = ""; }; @@ -514,6 +516,7 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( + 53F866422687A45400DCD1D7 /* Components */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, @@ -586,6 +589,14 @@ name = Frameworks; sourceTree = ""; }; + 53F866422687A45400DCD1D7 /* Components */ = { + isa = PBXGroup; + children = ( + 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, + ); + path = Components; + sourceTree = ""; + }; 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( @@ -992,6 +1003,7 @@ 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, + 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index 89ecb7e2..1ec19458 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -11,12 +11,14 @@ import SwiftUI struct LibraryFilterView: View { @Environment(\.presentationMode) var presentationMode @Binding var filters: LibraryFilters + var parentId: String = "" @StateObject var viewModel: LibraryFilterViewModel - init(filters: Binding, enabledFilterType: [FilterType]) { + init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { _filters = filters - _viewModel = StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType)) + self.parentId = parentId + _viewModel = StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) } var body: some View { diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 9b7c450b..f3b833aa 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -56,32 +56,38 @@ struct LibraryListView: View { .shadow(radius: 5) .padding(.bottom, 15) - ForEach(viewModel.libraries, id: \.id) { library in - if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") - }) { - ZStack { - ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) - .opacity(0.4) - HStack { - Spacer() - Text(library.name ?? "") - .foregroundColor(.white) - .font(.title2) - .fontWeight(.semibold) - Spacer() - }.padding(32) - }.background(Color.black) - .frame(minWidth: 100, maxWidth: .infinity) - .frame(height: 72) + if(!viewModel.isLoading) { + ForEach(viewModel.libraries, id: \.id) { library in + if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") + }) { + ZStack { + ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) + .opacity(0.4) + HStack { + Spacer() + VStack() { + Text(library.name ?? "") + .foregroundColor(.white) + .font(.title2) + .fontWeight(.semibold) + } + Spacer() + }.padding(32) + }.background(Color.black) + .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 100) + } + .cornerRadius(10) + .shadow(radius: 5) + .padding(.bottom, 5) + } else { + EmptyView() } - .cornerRadius(10) - .shadow(radius: 5) - .padding(.bottom, 5) - } else { - EmptyView() } + } else { + ProgressView() } }.padding(.leading, 16) .padding(.trailing, 16) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 59b12808..6a8c6126 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -30,36 +30,7 @@ struct LibrarySearchView: View { Spacer().frame(height: 16) LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .overlay( - ZStack { - if item.userData!.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } - }.padding(2) - .opacity(1), alignment: .topTrailing).opacity(1) - Text(item.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.productionYear != nil { - Text(String(item.productionYear!)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text(item.type ?? "") - } - }.frame(width: 100) - } + PortraitItemView(item: item) } Spacer().frame(height: 16) } diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 576b27a9..4cfd2385 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -34,38 +34,8 @@ struct LibraryView: View { Spacer().frame(height: 16) LazyVGrid(columns: tracks) { ForEach(viewModel.items, id: \.id) { item in - NavigationLink(destination: ItemView(item: item)) { - VStack(alignment: .leading) { - ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .overlay( - ZStack { - if item.userData!.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } - }.padding(2) - .opacity(1), alignment: .topTrailing).opacity(1) - Text(item.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.productionYear != nil { - Text(String(item.productionYear!)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text(item.type ?? "") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } - }.frame(width: 100) + if(item.type != "Folder") { + PortraitItemView(item: item) } } }.onRotate { _ in @@ -82,8 +52,8 @@ struct LibraryView: View { .font(.system(size: 25)) }.disabled(!viewModel.hasPreviousPage) Text("Page \(String(viewModel.currentPage + 1)) of \(String(viewModel.totalPages))") - .font(.headline) - .fontWeight(.semibold) + .font(.subheadline) + .fontWeight(.medium) Button { viewModel.requestNextPage() } label: { @@ -109,14 +79,14 @@ struct LibraryView: View { viewModel.requestPreviousPage() } label: { Image(systemName: "chevron.left") - } + }.disabled(viewModel.isLoading) } if viewModel.hasNextPage { Button { viewModel.requestNextPage() } label: { Image(systemName: "chevron.right") - } + }.disabled(viewModel.isLoading) } Label("Icon One", systemImage: "line.horizontal.3.decrease.circle") .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) @@ -131,7 +101,7 @@ struct LibraryView: View { } } .sheet(isPresented: $isShowingFilterView) { - LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType) + LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "") } .background( NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)), diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 7fbd811d..c0301df1 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -22,24 +22,7 @@ struct NextUpView: View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(items, id: \.id) { item in - NavigationLink(destination: LazyView { ItemView(item: item) }) { - VStack(alignment: .leading) { - ImageView(src: item.getSeriesPrimaryImage(maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 4) - Text(item.seriesName!) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - }.frame(width: 100) - } + PortraitItemView(item: item) }.padding(.trailing, 16) } .padding(.leading, 20) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 451756fc..3ad6bd5e 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -73,12 +73,27 @@ struct SeasonItemView: View { Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) .mask(ProgressBar()) - .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) + .frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7) .padding(0), alignment: .bottomLeading ) .overlay( ZStack { - if episode.userData!.played ?? false { + if episode.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } + } + .padding(.leading, 2) + .padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1) + , alignment: .bottomLeading) + .overlay( + ZStack { + if episode.userData?.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 591d5db9..84e65641 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -24,25 +24,7 @@ struct SeriesItemView: View { Spacer().frame(height: 16) LazyVGrid(columns: tracks) { ForEach(viewModel.seasons, id: \.id) { season in - NavigationLink(destination: ItemView(item: season)) { - VStack(alignment: .leading) { - ImageView(src: season.getPrimaryImage(maxWidth: 100), bh: season.getPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 5) - Text(season.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if season.productionYear != nil { - Text(String(season.productionYear!)) - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } - }.frame(width: 100) - } + PortraitItemView(item: season) } Spacer().frame(height: 2) }.onRotate { _ in diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index b4db6390..a45b0880 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -111,7 +111,7 @@ extension BaseItemDto { func getItemRuntime() -> String { let timeHMSFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() - formatter.unitsStyle = .brief + formatter.unitsStyle = .abbreviated formatter.allowedUnits = [.hour, .minute] return formatter }() diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index bf6a01ec..afbc206a 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -39,6 +39,8 @@ final class LibraryFilterViewModel: ViewModel { var selectedSortOrder: APISortOrder = .descending @Published var selectedSortBy: SortBy = .name + + var parentId: String = "" func updateModifiedFilter() { modifiedFilters.sortOrder = [selectedSortOrder] @@ -50,10 +52,11 @@ final class LibraryFilterViewModel: ViewModel { } init(filters: LibraryFilters? = nil, - enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter]) { + enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], parentId: String) { self.enabledFilterType = enabledFilterType self.selectedSortBy = filters!.sortBy.first! self.selectedSortOrder = filters!.sortOrder.first! + self.parentId = parentId super.init() if let filters = filters { @@ -63,7 +66,7 @@ final class LibraryFilterViewModel: ViewModel { } func requestQueryFilters() { - FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!) + FilterAPI.getQueryFilters(userId: SessionManager.current.user.user_id!, parentId: self.parentId) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestCompletion(completion: completion) diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 3ce566f9..e5921998 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -69,10 +69,10 @@ final class LibraryViewModel: ViewModel { } let sortBy = filters.sortBy.map(\.rawValue) - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, + ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: filters.filters.contains(.isFavorite) ? true : false, searchTerm: nil, sortOrder: filters.sortOrder, parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: sortBy, tags: filters.tags, + filters: filters.filters, sortBy: sortBy, tags: filters.tags, enableUserData: true, personIds: personIDs, studioIds: studioIDs, genreIds: genreIDs, enableImages: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in diff --git a/Shared/ViewModels/SeriesItemViewModel.swift b/Shared/ViewModels/SeriesItemViewModel.swift index aca9c6d3..b041f754 100644 --- a/Shared/ViewModels/SeriesItemViewModel.swift +++ b/Shared/ViewModels/SeriesItemViewModel.swift @@ -26,7 +26,7 @@ final class SeriesItemViewModel: ViewModel { } func requestSeasons() { - TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people]) + TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.current.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestCompletion(completion: completion)