diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 5450a054..307073b9 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -102,6 +102,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 */; }; @@ -287,6 +288,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 = ""; }; @@ -406,23 +408,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 = ""; @@ -496,10 +498,10 @@ 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, + C78797A232E2B8774099D1E9 /* Pods */, 5377CBF2263B596A003A4E83 /* Products */, 535870752669D60C00D05A09 /* Shared */, 628B95252670CABD0091AF3B /* WidgetExtension */, - C78797A232E2B8774099D1E9 /* Pods */, ); sourceTree = ""; }; @@ -516,6 +518,7 @@ 5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = { isa = PBXGroup; children = ( + 53F866422687A45400DCD1D7 /* Components */, 53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, @@ -589,6 +592,14 @@ name = Frameworks; sourceTree = ""; }; + 53F866422687A45400DCD1D7 /* Components */ = { + isa = PBXGroup; + children = ( + 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, + ); + path = Components; + sourceTree = ""; + }; 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( @@ -995,6 +1006,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/README.md b/README.md index 229dad6d..71fc5ac7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,6 @@ --- -[Join the Discord!](https://discord.gg/aWzcSzjjPN) - +[Join the Jellyfin Discord!](https://discord.gg/zHBxVSXdBV) +Also available on Matrix, and IRC. See https://jellyfin.org/contact for options. [Beta test!](https://testflight.apple.com/join/WiN0G62Q) 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)