diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj index cee92ab..4fe0af5 100644 --- a/Jel.xcodeproj/project.pbxproj +++ b/Jel.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 3D3816CE2B4B78BB006414D7 /* VisibilityTrackingScrollView in Frameworks */ = {isa = PBXBuildFile; productRef = 3D3816CD2B4B78BB006414D7 /* VisibilityTrackingScrollView */; }; 3D41D1FA2B2CAE0000E58234 /* ItemIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F92B2CAE0000E58234 /* ItemIconView.swift */; }; 3D4C15722B3CAA670035373E /* DashboardSectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4C15712B3CAA670035373E /* DashboardSectionTitleView.swift */; }; + 3D58F07E2B4DB19300DB2936 /* TextRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D58F07D2B4DB19300DB2936 /* TextRatingView.swift */; }; 3D7709392B29139700199889 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7709382B29139700199889 /* Pulse */; }; 3D77093B2B29139700199889 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3D77093A2B29139700199889 /* PulseUI */; }; 3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */; }; @@ -38,10 +39,11 @@ 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */; }; 3DAFA8E82B38AFED00D71AD1 /* ItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAFA8E72B38AFED00D71AD1 /* ItemInfoView.swift */; }; 3DAFA8EA2B39039900D71AD1 /* JellyfinKitExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAFA8E92B39039900D71AD1 /* JellyfinKitExtensions.swift */; }; - 3DAFA8EC2B394F9F00D71AD1 /* ViewConditionalMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAFA8EB2B394F9F00D71AD1 /* ViewConditionalMethod.swift */; }; + 3DAFA8EC2B394F9F00D71AD1 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAFA8EB2B394F9F00D71AD1 /* ViewExtensions.swift */; }; 3DAFA8EF2B3B707B00D71AD1 /* ItemMovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAFA8EE2B3B707B00D71AD1 /* ItemMovieView.swift */; }; 3DBAC9E22B4C31BE005F8764 /* ItemPeopleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBAC9E12B4C31BE005F8764 /* ItemPeopleView.swift */; }; 3DBAC9E42B4C7404005F8764 /* UIScreenCurrent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBAC9E32B4C7404005F8764 /* UIScreenCurrent.swift */; }; + 3DBAC9EA2B4C8927005F8764 /* ItemPersonIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBAC9E92B4C8927005F8764 /* ItemPersonIconView.swift */; }; 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC6BA2C2B2A422300416B9F /* SettingsController.swift */; }; 3DDD67932B293BC40026781E /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDD67922B293BC40026781E /* DashboardView.swift */; }; 3DDD67962B29E28B0026781E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDD67952B29E28B0026781E /* SettingsView.swift */; }; @@ -91,6 +93,7 @@ 3D3816C82B4B5648006414D7 /* ItemGenresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemGenresView.swift; sourceTree = ""; }; 3D41D1F92B2CAE0000E58234 /* ItemIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemIconView.swift; sourceTree = ""; }; 3D4C15712B3CAA670035373E /* DashboardSectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardSectionTitleView.swift; sourceTree = ""; }; + 3D58F07D2B4DB19300DB2936 /* TextRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRatingView.swift; sourceTree = ""; }; 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 3D8AB2A72B366353005BD7D0 /* LibraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryDetailView.swift; sourceTree = ""; }; 3D9063C72B279A310063DD2A /* Jel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jel.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -109,10 +112,11 @@ 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinDateFormatter.swift; sourceTree = ""; }; 3DAFA8E72B38AFED00D71AD1 /* ItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInfoView.swift; sourceTree = ""; }; 3DAFA8E92B39039900D71AD1 /* JellyfinKitExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinKitExtensions.swift; sourceTree = ""; }; - 3DAFA8EB2B394F9F00D71AD1 /* ViewConditionalMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewConditionalMethod.swift; sourceTree = ""; }; + 3DAFA8EB2B394F9F00D71AD1 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; 3DAFA8EE2B3B707B00D71AD1 /* ItemMovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemMovieView.swift; sourceTree = ""; }; 3DBAC9E12B4C31BE005F8764 /* ItemPeopleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPeopleView.swift; sourceTree = ""; }; 3DBAC9E32B4C7404005F8764 /* UIScreenCurrent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenCurrent.swift; sourceTree = ""; }; + 3DBAC9E92B4C8927005F8764 /* ItemPersonIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPersonIconView.swift; sourceTree = ""; }; 3DC0E5802B2832B9001CCE96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3DC6BA2C2B2A422300416B9F /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; 3DDD67922B293BC40026781E /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; @@ -182,7 +186,7 @@ 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */, 3D13F9682B389FA300E91913 /* ViewOffsetKey.swift */, 3DAFA8E92B39039900D71AD1 /* JellyfinKitExtensions.swift */, - 3DAFA8EB2B394F9F00D71AD1 /* ViewConditionalMethod.swift */, + 3DAFA8EB2B394F9F00D71AD1 /* ViewExtensions.swift */, ); path = Models; sourceTree = ""; @@ -191,12 +195,12 @@ isa = PBXGroup; children = ( 3DAFA8ED2B3B707100D71AD1 /* Types */, + 3DBAC9E82B4C891C005F8764 /* Person */, 3D13F95E2B375DB800E91913 /* ItemView.swift */, 3D13F9602B37637500E91913 /* ItemMediaView.swift */, 3DAFA8E72B38AFED00D71AD1 /* ItemInfoView.swift */, 3D13F9642B37EC7A00E91913 /* ItemHeaderView.swift */, 3D3816C82B4B5648006414D7 /* ItemGenresView.swift */, - 3DBAC9E12B4C31BE005F8764 /* ItemPeopleView.swift */, ); path = Item; sourceTree = ""; @@ -205,6 +209,7 @@ isa = PBXGroup; children = ( 3D13F96E2B38A32500E91913 /* StickyHeaderView.swift */, + 3D58F07D2B4DB19300DB2936 /* TextRatingView.swift */, ); path = Utility; sourceTree = ""; @@ -298,6 +303,15 @@ path = Types; sourceTree = ""; }; + 3DBAC9E82B4C891C005F8764 /* Person */ = { + isa = PBXGroup; + children = ( + 3DBAC9E12B4C31BE005F8764 /* ItemPeopleView.swift */, + 3DBAC9E92B4C8927005F8764 /* ItemPersonIconView.swift */, + ); + path = Person; + sourceTree = ""; + }; 3DDD67902B293B780026781E /* Dashboard */ = { isa = PBXGroup; children = ( @@ -467,11 +481,12 @@ 3DAFA8EA2B39039900D71AD1 /* JellyfinKitExtensions.swift in Sources */, 3D13F9652B37EC7A00E91913 /* ItemHeaderView.swift in Sources */, 3D3816C92B4B5648006414D7 /* ItemGenresView.swift in Sources */, - 3DAFA8EC2B394F9F00D71AD1 /* ViewConditionalMethod.swift in Sources */, + 3DAFA8EC2B394F9F00D71AD1 /* ViewExtensions.swift in Sources */, 3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */, 3DBAC9E42B4C7404005F8764 /* UIScreenCurrent.swift in Sources */, 3D13F9692B389FA300E91913 /* ViewOffsetKey.swift in Sources */, 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */, + 3D58F07E2B4DB19300DB2936 /* TextRatingView.swift in Sources */, 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */, 3DAFA8EF2B3B707B00D71AD1 /* ItemMovieView.swift in Sources */, 3D8AB2A82B366353005BD7D0 /* LibraryDetailView.swift in Sources */, @@ -479,6 +494,7 @@ 3D13F9612B37637500E91913 /* ItemMediaView.swift in Sources */, 3D41D1FA2B2CAE0000E58234 /* ItemIconView.swift in Sources */, 3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */, + 3DBAC9EA2B4C8927005F8764 /* ItemPersonIconView.swift in Sources */, 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */, 3DAFA8E82B38AFED00D71AD1 /* ItemInfoView.swift in Sources */, 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */, diff --git a/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist b/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist index 27d21d3..4bb5b56 100644 --- a/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Jel.xcscheme_^#shared#^_ orderHint - 0 + 1 JellyfinClient.xcscheme_^#shared#^_ diff --git a/Jel/Models/ViewConditionalMethod.swift b/Jel/Models/ViewExtensions.swift similarity index 51% rename from Jel/Models/ViewConditionalMethod.swift rename to Jel/Models/ViewExtensions.swift index 195a8dc..7f54865 100644 --- a/Jel/Models/ViewConditionalMethod.swift +++ b/Jel/Models/ViewExtensions.swift @@ -1,5 +1,5 @@ // -// ViewConditionalMethod.swift +// ViewExtensions.swift // Jel // // Created by zerocool on 12/25/23. @@ -9,10 +9,6 @@ import SwiftUI extension View { /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { if condition() { transform(self) @@ -21,3 +17,15 @@ extension View { } } } + +extension View { + /// Applies an inverse mask to the given view + public func inverseMask(_ mask: Content) -> some View { + let inverseMask = mask + .foregroundStyle(.black) + .background(.white) + .compositingGroup() + .luminanceToAlpha() + return self.mask(inverseMask) + } +} diff --git a/Jel/Views/Library/Item/ItemGenresView.swift b/Jel/Views/Library/Item/ItemGenresView.swift index 5bfccb0..4e8321f 100644 --- a/Jel/Views/Library/Item/ItemGenresView.swift +++ b/Jel/Views/Library/Item/ItemGenresView.swift @@ -20,6 +20,7 @@ struct ItemGenresView: View { VStack(alignment: .leading) { Text("Genres") .font(.title2) + .padding(.horizontal) ScrollView(.horizontal) { HStack { @@ -43,18 +44,15 @@ struct ItemGenresView: View { .clipShape(.capsule) } } + .padding(.horizontal) } + .scrollIndicators(.hidden) } .onAppear { Task { let parameters = Paths.GetItemsParameters( userID: authState.userId ?? "", isRecursive: true, - fields: [.primaryImageAspectRatio, - .genres, - .taglines, - .overview, - .parentID], includeItemTypes: [.movie, .series], genres: item.genres ?? [] ) diff --git a/Jel/Views/Library/Item/ItemHeaderView.swift b/Jel/Views/Library/Item/ItemHeaderView.swift index 44be776..4c2bbe3 100644 --- a/Jel/Views/Library/Item/ItemHeaderView.swift +++ b/Jel/Views/Library/Item/ItemHeaderView.swift @@ -25,7 +25,7 @@ struct ItemHeaderView: View { StickyHeaderView(minHeight: 400) { ItemIconView(item: item, imageType: "Backdrop", contentMode: .fill) .setCornerRadius(0) - .overlay(overlayGradient.opacity(0.8)) + .overlay(overlayGradient) } HStack { diff --git a/Jel/Views/Library/Item/ItemInfoView.swift b/Jel/Views/Library/Item/ItemInfoView.swift index bafbc9f..dda1c39 100644 --- a/Jel/Views/Library/Item/ItemInfoView.swift +++ b/Jel/Views/Library/Item/ItemInfoView.swift @@ -24,14 +24,7 @@ struct ItemInfoView: View { Text(item.getRuntime() ?? "-:--") } if let officialRating = item.officialRating { - Text(officialRating) - .bold() - .padding(2) - .overlay { - RoundedRectangle(cornerSize: CGSize(width: 2, height: 2), style: .continuous) - .stroke(.gray) - } - .foregroundStyle(.gray) + TextRatingView(officialRating, fillStyle: .stroke) } } } diff --git a/Jel/Views/Library/Item/ItemPeopleView.swift b/Jel/Views/Library/Item/ItemPeopleView.swift deleted file mode 100644 index b8b0846..0000000 --- a/Jel/Views/Library/Item/ItemPeopleView.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ItemPeopleView.swift -// Jel -// -// Created by zerocool on 1/8/24. -// - -import SwiftUI -import JellyfinKit - -struct ItemPeopleView: View { - var item: BaseItemDto - - var body: some View { - ScrollView(.horizontal) { - HStack { - ForEach(item.people ?? []) {person in - Text(person.name ?? "---") - } - } - } - } -} - -//#Preview { -// ItemPeopleView() -//} diff --git a/Jel/Views/Library/Item/ItemView.swift b/Jel/Views/Library/Item/ItemView.swift index de4dc1b..da85f32 100644 --- a/Jel/Views/Library/Item/ItemView.swift +++ b/Jel/Views/Library/Item/ItemView.swift @@ -21,6 +21,7 @@ struct ItemView: View { } } } + .scrollIndicators(.hidden) } } diff --git a/Jel/Views/Library/Item/Person/ItemPeopleView.swift b/Jel/Views/Library/Item/Person/ItemPeopleView.swift new file mode 100644 index 0000000..6e2a974 --- /dev/null +++ b/Jel/Views/Library/Item/Person/ItemPeopleView.swift @@ -0,0 +1,37 @@ +// +// ItemPeopleView.swift +// Jel +// +// Created by zerocool on 1/8/24. +// + +import SwiftUI +import JellyfinKit +import NukeUI + +struct ItemPeopleView: View { + + var item: BaseItemDto + + var body: some View { + VStack(alignment: .leading) { + Text("Cast and Crew") + .font(.title2) + .padding(.leading) + + ScrollView(.horizontal) { + LazyHStack(alignment: .top) { + ForEach(item.people ?? [], id: \.iterId) {person in + ItemPersonIconView(person: person) + } + } + .padding(.horizontal) + } + .scrollIndicators(.hidden) + } + } +} + +//#Preview { +// ItemPeopleView() +//} diff --git a/Jel/Views/Library/Item/Person/ItemPersonIconView.swift b/Jel/Views/Library/Item/Person/ItemPersonIconView.swift new file mode 100644 index 0000000..a6e5161 --- /dev/null +++ b/Jel/Views/Library/Item/Person/ItemPersonIconView.swift @@ -0,0 +1,70 @@ +// +// ItemPersonIconView.swift +// Jel +// +// Created by zerocool on 1/8/24. +// + +import SwiftUI +import JellyfinKit +import NukeUI + +struct ItemPersonIconPlaceholderView: View { + var body: some View { + VStack { + Image(systemName: "person") + .resizable() + .padding() + .scaledToFit() + } + } +} + +struct ItemPersonIconView: View { + @StateObject var authState: AuthStateController = AuthStateController.shared + @EnvironmentObject var jellyfinClient: JellyfinClientController + + var person: BaseItemPerson + + @State var personImageUrl: URL? + @State var loading: Bool = true + + var body: some View { + VStack() { + LazyImage(url: personImageUrl) {state in + if let image = state.image { + image + .resizable() + .clipShape(RoundedRectangle(cornerRadius: 5)) + } else { + ItemPersonIconPlaceholderView() + } + } + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 170) + + VStack { + Text(person.name ?? "---") + .font(.callout) + Text(person.role ?? "---") + .font(.caption) + .foregroundStyle(.gray) + } + .frame(width: 100) + } + // .redacted(reason: loading ? .placeholder : []) + .onAppear { + Task { + let request = Paths.getItemImage(itemID: person.id ?? "", imageType: "Primary") + + let serverUrl = jellyfinClient.getUrl() + personImageUrl = serverUrl?.appending(path: request.url?.absoluteString ?? "") + // loading = false + } + } + } +} + +//#Preview { +// ItemPersonView() +//} diff --git a/Jel/Views/Library/Item/Types/ItemMovieView.swift b/Jel/Views/Library/Item/Types/ItemMovieView.swift index fcf8c06..5181e73 100644 --- a/Jel/Views/Library/Item/Types/ItemMovieView.swift +++ b/Jel/Views/Library/Item/Types/ItemMovieView.swift @@ -23,7 +23,7 @@ struct ItemMovieView: View { .onChange(of: geo.frame(in: .global).minY) { let minY = geo.frame(in: .global).minY - pageScrolled = minY < 0 + pageScrolled = minY < -100 } } } @@ -32,10 +32,8 @@ struct ItemMovieView: View { .padding() ItemGenresView(item: item) - .padding() ItemPeopleView(item: item) - .padding() } .navigationBarTitleDisplayMode(.inline) .navigationTitle(item.name ?? "Untitled") diff --git a/Jel/Views/Library/ItemIconView.swift b/Jel/Views/Library/ItemIconView.swift index ec3952c..c2006cc 100644 --- a/Jel/Views/Library/ItemIconView.swift +++ b/Jel/Views/Library/ItemIconView.swift @@ -22,7 +22,7 @@ struct ItemIconView: View { @State var imageUrl: URL? @State var contentMode: ContentMode = .fit - @State var placeHolder: AnyView? + var placeHolder: AnyView? var shouldShowCaption: Bool = false var imageCornerRadius: CGFloat = 5 diff --git a/Jel/Views/Library/LibraryDetailView.swift b/Jel/Views/Library/LibraryDetailView.swift index c85651d..5f0d9a7 100644 --- a/Jel/Views/Library/LibraryDetailView.swift +++ b/Jel/Views/Library/LibraryDetailView.swift @@ -83,13 +83,7 @@ struct LibraryDetailView: View { Task { let params = Paths.GetItemsParameters( userID: authState.userId, - parentID: library.id, - fields: [.primaryImageAspectRatio, - .genres, - .taglines, - .overview, - .parentID, - .people] + parentID: library.id ) let request = Paths.getItems(parameters: params) diff --git a/Jel/Views/Utility/TextRatingView.swift b/Jel/Views/Utility/TextRatingView.swift new file mode 100644 index 0000000..760d251 --- /dev/null +++ b/Jel/Views/Utility/TextRatingView.swift @@ -0,0 +1,58 @@ +// +// TextRatingView.swift +// Jel +// +// Created by zerocool on 1/9/24. +// + +import SwiftUI + +enum TextRatingViewStyle { + case stroke + case fill +} + +struct TextRatingView: View { + var text: String + var fillStyle: TextRatingViewStyle + + init(_ text: String, fillStyle: TextRatingViewStyle = .stroke) { + self.text = text + self.fillStyle = fillStyle + } + + var body: some View { + switch (fillStyle) { + case .stroke: + Text(text) + .font(.caption) + .bold() + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay { + RoundedRectangle(cornerRadius: 2, style: .continuous) + .stroke(.gray, lineWidth: 1.5) + } + .foregroundStyle(.gray) + case .fill: + Text(text) + .font(.caption) + .bold() + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .hidden() + .background { + Color(.gray) + .clipShape(RoundedRectangle(cornerRadius: 2, style: .continuous)) + .inverseMask( + Text(text) + .font(.caption) + .bold() + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + ) + } + } + } +} + +//#Preview { +// TextRatingView() +//}