diff options
Diffstat (limited to 'Jel/Views/Item')
-rw-r--r-- | Jel/Views/Item/ItemGenresView.swift | 73 | ||||
-rw-r--r-- | Jel/Views/Item/ItemHeaderView.swift | 52 | ||||
-rw-r--r-- | Jel/Views/Item/ItemIconView.swift | 106 | ||||
-rw-r--r-- | Jel/Views/Item/ItemInfoView.swift | 37 | ||||
-rw-r--r-- | Jel/Views/Item/ItemMediaView.swift | 34 | ||||
-rw-r--r-- | Jel/Views/Item/ItemView.swift | 30 | ||||
-rw-r--r-- | Jel/Views/Item/Person/ItemPeopleView.swift | 46 | ||||
-rw-r--r-- | Jel/Views/Item/Person/ItemPersonIconView.swift | 75 | ||||
-rw-r--r-- | Jel/Views/Item/Types/ItemMovieView.swift | 57 |
9 files changed, 510 insertions, 0 deletions
diff --git a/Jel/Views/Item/ItemGenresView.swift b/Jel/Views/Item/ItemGenresView.swift new file mode 100644 index 0000000..4e8321f --- /dev/null +++ b/Jel/Views/Item/ItemGenresView.swift @@ -0,0 +1,73 @@ +// +// ItemGenresView.swift +// Jel +// +// Created by zerocool on 1/7/24. +// + +import SwiftUI +import JellyfinKit + +struct ItemGenresView: View { + @EnvironmentObject var jellyfinClient: JellyfinClientController + + @StateObject var authState: AuthStateController = AuthStateController.shared + + var item: BaseItemDto + @State var libraryItems: [BaseItemDto]? = [] + + var body: some View { + VStack(alignment: .leading) { + Text("Genres") + .font(.title2) + .padding(.horizontal) + + ScrollView(.horizontal) { + HStack { + ForEach(item.genres ?? [], id: \.self) {genre in + NavigationLink { + LibraryDetailView(library: BaseItemDto(name: genre), items: libraryItems) {items in + var matchingItems: [BaseItemDto] = [] + + for item in items { + if (item.genres ?? []).contains(genre) { + matchingItems.append(item) + } + } + return matchingItems + } + .navigationTitle(genre) + } label: { + Text(genre) + } + .buttonStyle(.bordered) + .clipShape(.capsule) + } + } + .padding(.horizontal) + } + .scrollIndicators(.hidden) + } + .onAppear { + Task { + let parameters = Paths.GetItemsParameters( + userID: authState.userId ?? "", + isRecursive: true, + includeItemTypes: [.movie, .series], + genres: item.genres ?? [] + ) + let request = Paths.getItems(parameters: parameters) + do { + let res = try await jellyfinClient.send(request) + libraryItems = res.value.items ?? [] + } catch { + } + } + + } + } +} + +//#Preview { +// ItemGenresView() +//} diff --git a/Jel/Views/Item/ItemHeaderView.swift b/Jel/Views/Item/ItemHeaderView.swift new file mode 100644 index 0000000..4c2bbe3 --- /dev/null +++ b/Jel/Views/Item/ItemHeaderView.swift @@ -0,0 +1,52 @@ +// +// ItemHeaderView.swift +// Jel +// +// Created by zerocool on 12/23/23. +// + +import SwiftUI +import JellyfinKit + +struct ItemHeaderView: View { + var item: BaseItemDto + + let overlayGradientMask = LinearGradient(gradient: Gradient(stops: [ + .init(color: .clear, location: 0), + .init(color: .black, location: 0.3), + ]), startPoint: .bottom, endPoint: .top) + let overlayGradient = LinearGradient(gradient: Gradient(stops: [ + .init(color: .black, location: 0), + .init(color: .clear, location: 0.5) + ]), startPoint: .bottom, endPoint: .top) + + var body: some View { + ZStack(alignment: .bottom) { + StickyHeaderView(minHeight: 400) { + ItemIconView(item: item, imageType: "Backdrop", contentMode: .fill) + .setCornerRadius(0) + .overlay(overlayGradient) + } + + HStack { + ItemIconView(item: item, imageType: "Logo", width: 200, height: 100, placeHolder: AnyView(Text(item.name ?? "Unknown").font(.title).bold().truncationMode(.middle))) + .setCornerRadius(0) + .shadow(radius: 10) + .frame(alignment: .leading) + Spacer() + ItemInfoView(item: item) + } + .padding([.leading, .trailing]) + } +// .scrollTransition {content, phase in +// content +// .scaleEffect(phase.isIdentity ? 1 : 2) +// .opacity(phase.isIdentity ? 1 : 0.1) +// .blur(radius: phase.isIdentity ? 0 : 50) +// } + } +} + +// #Preview { +// ItemHeaderView(item: BaseItemDto()) +// } diff --git a/Jel/Views/Item/ItemIconView.swift b/Jel/Views/Item/ItemIconView.swift new file mode 100644 index 0000000..c2006cc --- /dev/null +++ b/Jel/Views/Item/ItemIconView.swift @@ -0,0 +1,106 @@ +// +// ItemIconView.swift +// Jel +// +// Created by zerocool on 12/15/23. +// + +import SwiftUI +import JellyfinKit +import NukeUI + +struct ItemIconView: View { + @EnvironmentObject var jellyfinClient: JellyfinClientController + + var item: BaseItemDto + + var imageType: String = "Primary" + var width: CGFloat? + var height: CGFloat? + + @State var blurHashImage: UIImage = UIImage() + @State var imageUrl: URL? + @State var contentMode: ContentMode = .fit + + var placeHolder: AnyView? + + var shouldShowCaption: Bool = false + var imageCornerRadius: CGFloat = 5 + var body: some View { + VStack(alignment: .leading) { + LazyImage(url: imageUrl) {state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: contentMode) + } else { + if let content = placeHolder { + content + } else { + Image(uiImage: blurHashImage) + .resizable() + .aspectRatio(contentMode: .fill) + } + } + } + .frame(width: width, height: height) + .clipShape(RoundedRectangle(cornerRadius: imageCornerRadius)) + .onAppear { + let blurhash = getBlurHash(imageType: imageType) + blurHashImage = UIImage(blurHash: blurhash, size: CGSize(width: 32, height: 32)) ?? UIImage() + + let imageId = item.id ?? "" + let request = Paths.getItemImage(itemID: imageId, imageType: imageType) + imageUrl = jellyfinClient.getUrl()?.appending(path: request.url?.absoluteString ?? "") + } + + if shouldShowCaption { + Text(item.name ?? "Unknown") + .font(.subheadline) + } + } + } + + private func getBlurHash(imageType: String) -> String { + switch imageType { + case "Primary": + return item.imageBlurHashes?.primary?[item.imageTags?[imageType] ?? ""] ?? "" + case "Backdrop": + return item.imageBlurHashes?.backdrop?[item.backdropImageTags?[0] ?? ""] ?? "" + default: + return "" + } + } + + func showCaption(_ showCaption: Bool = true) -> Self { + var copy = self + copy.shouldShowCaption = showCaption + return copy + } + + func setCornerRadius(_ cornerRadius: CGFloat = 5) -> Self { + var copy = self + copy.imageCornerRadius = cornerRadius + return copy + } + + func setAspectRatio(_ aspectRatio: Double?) -> Self { + var copy = self + if aspectRatio == nil { + return copy + } + + if let newWidth = copy.width { + copy.height = newWidth / aspectRatio! + } + if let newHeight = copy.height { + copy.width = newHeight * aspectRatio! + } + + return copy + } +} + +//#Preview { +// LibraryIconView(library: BaseItemDto()) +//} diff --git a/Jel/Views/Item/ItemInfoView.swift b/Jel/Views/Item/ItemInfoView.swift new file mode 100644 index 0000000..dda1c39 --- /dev/null +++ b/Jel/Views/Item/ItemInfoView.swift @@ -0,0 +1,37 @@ +// +// ItemInfoView.swift +// Jel +// +// Created by zerocool on 12/24/23. +// + +import SwiftUI +import JellyfinKit + +struct ItemInfoView: View { + var item: BaseItemDto + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text((item.productionYear != nil) ? String(item.productionYear!) : "---") + Text("•") + Text(item.genres?.first ?? "---") + } + + HStack { + if item.type == .movie { + Text(item.getRuntime() ?? "-:--") + } + if let officialRating = item.officialRating { + TextRatingView(officialRating, fillStyle: .stroke) + } + } + } + .font(.caption) + } +} + +//#Preview { +// ItemInfoView() +//} diff --git a/Jel/Views/Item/ItemMediaView.swift b/Jel/Views/Item/ItemMediaView.swift new file mode 100644 index 0000000..be8264a --- /dev/null +++ b/Jel/Views/Item/ItemMediaView.swift @@ -0,0 +1,34 @@ +// +// ItemMediaView.swift +// Jel +// +// Created by zerocool on 12/23/23. +// + +import SwiftUI +import JellyfinKit +import VisibilityTrackingScrollView + +struct ItemMediaView: View { + @EnvironmentObject var jellyfinClient: JellyfinClientController + @StateObject var authState: AuthStateController = AuthStateController.shared + + var item: BaseItemDto + + + var body: some View { + VStack(alignment: .leading) { + Text(item.taglines?.count ?? 0 > 0 ? item.taglines?[0] ?? "" : "") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + ForEach(item.overview?.components(separatedBy: "<br>") ?? [], id: \.self) {overview in + Text(overview) + } + } + } +} + +//#Preview { +// ItemMovieView(item: BaseItemDto()) +//} diff --git a/Jel/Views/Item/ItemView.swift b/Jel/Views/Item/ItemView.swift new file mode 100644 index 0000000..da85f32 --- /dev/null +++ b/Jel/Views/Item/ItemView.swift @@ -0,0 +1,30 @@ +// +// ItemView.swift +// Jel +// +// Created by zerocool on 12/23/23. +// + +import SwiftUI +import JellyfinKit + +struct ItemView: View { + @State var item: BaseItemDto + var body: some View { + ScrollView { + VStack { + switch item.type { + case .movie: + ItemMovieView(item: item) + default: + ItemMediaView(item: item) + } + } + } + .scrollIndicators(.hidden) + } +} + +//#Preview { +// ItemView(item: BaseItemDto()) +//} diff --git a/Jel/Views/Item/Person/ItemPeopleView.swift b/Jel/Views/Item/Person/ItemPeopleView.swift new file mode 100644 index 0000000..f007796 --- /dev/null +++ b/Jel/Views/Item/Person/ItemPeopleView.swift @@ -0,0 +1,46 @@ +// +// 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) { + // FIXME: For some reason, a LazyHStack clips the text for this view + HStack(alignment: .top) { + ForEach(item.people ?? [], id: \.iterId) {person in + NavigationLink { + VStack { + ItemPersonIconView(person: person) + Text("Subview") + } + .navigationTitle(person.name ?? "Unnamed") + } label: { + ItemPersonIconView(person: person) + } + } + } + .padding(.horizontal) + } + .scrollIndicators(.hidden) + } + } +} + +//#Preview { +// ItemPeopleView() +//} diff --git a/Jel/Views/Item/Person/ItemPersonIconView.swift b/Jel/Views/Item/Person/ItemPersonIconView.swift new file mode 100644 index 0000000..b839deb --- /dev/null +++ b/Jel/Views/Item/Person/ItemPersonIconView.swift @@ -0,0 +1,75 @@ +// +// ItemPersonIconView.swift +// Jel +// +// Created by zerocool on 1/8/24. +// + +import SwiftUI +import JellyfinKit +import NukeUI + +struct ItemPersonIconPlaceholderView: View { + var body: some View { + ZStack { + Color(uiColor: UIColor.secondarySystemBackground) + Image(systemName: "person.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + .foregroundStyle(Color(uiColor: UIColor.secondarySystemFill)) + } + .frame(height: 150) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } +} + +struct ItemPersonIconView: View { + @StateObject var authState: AuthStateController = AuthStateController.shared + @EnvironmentObject var jellyfinClient: JellyfinClientController + + var person: BaseItemPerson + + @State var personImageUrl: URL? + + var body: some View { + VStack { + LazyImage(url: personImageUrl) {state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 5)) + } else { + ItemPersonIconPlaceholderView() + } + } + .frame(height: 170) + + VStack(alignment: .leading) { + Text(person.name ?? "---") + .font(.footnote) + .lineLimit(nil) + Text(person.role ?? "---") + .font(.caption) + .foregroundStyle(Color(uiColor: UIColor.secondaryLabel)) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(nil) + } + .multilineTextAlignment(.leading) + } + .frame(width: 100) + .onAppear { + Task { + let request = Paths.getItemImage(itemID: person.id ?? "", imageType: "Primary") + + let serverUrl = jellyfinClient.getUrl() + personImageUrl = serverUrl?.appending(path: request.url?.absoluteString ?? "") + } + } + } +} + +//#Preview { +// ItemPersonView() +//} diff --git a/Jel/Views/Item/Types/ItemMovieView.swift b/Jel/Views/Item/Types/ItemMovieView.swift new file mode 100644 index 0000000..5cb5c3b --- /dev/null +++ b/Jel/Views/Item/Types/ItemMovieView.swift @@ -0,0 +1,57 @@ +// +// ItemMovieView.swift +// Jel +// +// Created by zerocool on 12/26/23. +// + +import SwiftUI +import JellyfinKit + +struct ItemMovieView: View { + var item: BaseItemDto + + @State var pageScrolled: Bool = false + + var body: some View { + VStack { + ItemHeaderView(item: item) + .foregroundStyle(.white) + .background { + GeometryReader {geo in + EmptyView() + .onChange(of: geo.frame(in: .global).minY) { + let minY = geo.frame(in: .global).minY + + pageScrolled = minY < -150 + } + } + } + + ItemMediaView(item: item) + .padding() + + ItemGenresView(item: item) + .padding(.bottom) + .foregroundStyle(Color.primary) + + ItemPeopleView(item: item) + .padding(.bottom) + .foregroundStyle(Color.primary) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(item.name ?? "Untitled") + .toolbarRole(.editor) + .toolbar { + ToolbarItem(placement: .principal) { + Text(pageScrolled ? item.name ?? "Untitled" : "") + .bold() + } + } + .toolbarBackground(pageScrolled ? .visible : .hidden) + } +} + +//#Preview { +// ItemMovieView() +//} |