summaryrefslogtreecommitdiff
path: root/Jel/Views/Item
diff options
context:
space:
mode:
Diffstat (limited to 'Jel/Views/Item')
-rw-r--r--Jel/Views/Item/ItemGenresView.swift73
-rw-r--r--Jel/Views/Item/ItemHeaderView.swift52
-rw-r--r--Jel/Views/Item/ItemIconView.swift106
-rw-r--r--Jel/Views/Item/ItemInfoView.swift37
-rw-r--r--Jel/Views/Item/ItemMediaView.swift34
-rw-r--r--Jel/Views/Item/ItemView.swift30
-rw-r--r--Jel/Views/Item/Person/ItemPeopleView.swift46
-rw-r--r--Jel/Views/Item/Person/ItemPersonIconView.swift75
-rw-r--r--Jel/Views/Item/Types/ItemMovieView.swift57
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()
+//}