From 6b8d3372d21149ed0efb4d43bf0cab44bd24f9a4 Mon Sep 17 00:00:00 2001 From: Shav Kinderlehrer Date: Thu, 11 Jan 2024 20:44:49 -0500 Subject: Implement peopleView --- Jel.xcodeproj/project.pbxproj | 4 +- .../xcschemes/xcschememanagement.plist | 2 +- Jel/Views/Item/ItemGenresView.swift | 73 ++++++++++++++ Jel/Views/Item/ItemHeaderView.swift | 52 ++++++++++ Jel/Views/Item/ItemIconView.swift | 106 +++++++++++++++++++++ Jel/Views/Item/ItemInfoView.swift | 37 +++++++ Jel/Views/Item/ItemMediaView.swift | 34 +++++++ Jel/Views/Item/ItemView.swift | 30 ++++++ Jel/Views/Item/Person/ItemPeopleView.swift | 46 +++++++++ Jel/Views/Item/Person/ItemPersonIconView.swift | 75 +++++++++++++++ Jel/Views/Item/Types/ItemMovieView.swift | 57 +++++++++++ Jel/Views/Library/Item/ItemGenresView.swift | 73 -------------- Jel/Views/Library/Item/ItemHeaderView.swift | 52 ---------- Jel/Views/Library/Item/ItemInfoView.swift | 37 ------- Jel/Views/Library/Item/ItemMediaView.swift | 35 ------- Jel/Views/Library/Item/ItemView.swift | 30 ------ Jel/Views/Library/Item/Person/ItemPeopleView.swift | 37 ------- .../Library/Item/Person/ItemPersonIconView.swift | 70 -------------- Jel/Views/Library/Item/Types/ItemMovieView.swift | 53 ----------- Jel/Views/Library/ItemIconView.swift | 106 --------------------- TODO.txt | 10 ++ 21 files changed, 523 insertions(+), 496 deletions(-) create mode 100644 Jel/Views/Item/ItemGenresView.swift create mode 100644 Jel/Views/Item/ItemHeaderView.swift create mode 100644 Jel/Views/Item/ItemIconView.swift create mode 100644 Jel/Views/Item/ItemInfoView.swift create mode 100644 Jel/Views/Item/ItemMediaView.swift create mode 100644 Jel/Views/Item/ItemView.swift create mode 100644 Jel/Views/Item/Person/ItemPeopleView.swift create mode 100644 Jel/Views/Item/Person/ItemPersonIconView.swift create mode 100644 Jel/Views/Item/Types/ItemMovieView.swift delete mode 100644 Jel/Views/Library/Item/ItemGenresView.swift delete mode 100644 Jel/Views/Library/Item/ItemHeaderView.swift delete mode 100644 Jel/Views/Library/Item/ItemInfoView.swift delete mode 100644 Jel/Views/Library/Item/ItemMediaView.swift delete mode 100644 Jel/Views/Library/Item/ItemView.swift delete mode 100644 Jel/Views/Library/Item/Person/ItemPeopleView.swift delete mode 100644 Jel/Views/Library/Item/Person/ItemPersonIconView.swift delete mode 100644 Jel/Views/Library/Item/Types/ItemMovieView.swift delete mode 100644 Jel/Views/Library/ItemIconView.swift create mode 100644 TODO.txt diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj index 4fe0af5..6237f55 100644 --- a/Jel.xcodeproj/project.pbxproj +++ b/Jel.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ 3D13F96D2B38A31300E91913 /* Utility */, 3D9063CC2B279A310063DD2A /* ContentView.swift */, 3DDD67902B293B780026781E /* Dashboard */, + 3D13F95D2B375DAC00E91913 /* Item */, 3D8AB2A62B366309005BD7D0 /* Library */, 3DDD67942B29E27A0026781E /* Settings */, 3D91FDC52B28C28900919017 /* SignIn */, @@ -194,6 +195,7 @@ 3D13F95D2B375DAC00E91913 /* Item */ = { isa = PBXGroup; children = ( + 3D41D1F92B2CAE0000E58234 /* ItemIconView.swift */, 3DAFA8ED2B3B707100D71AD1 /* Types */, 3DBAC9E82B4C891C005F8764 /* Person */, 3D13F95E2B375DB800E91913 /* ItemView.swift */, @@ -217,8 +219,6 @@ 3D8AB2A62B366309005BD7D0 /* Library */ = { isa = PBXGroup; children = ( - 3D13F95D2B375DAC00E91913 /* Item */, - 3D41D1F92B2CAE0000E58234 /* ItemIconView.swift */, 3D8AB2A72B366353005BD7D0 /* LibraryDetailView.swift */, ); path = Library; diff --git a/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist b/Jel.xcodeproj/xcuserdata/zerocool.xcuserdatad/xcschemes/xcschememanagement.plist index 4bb5b56..27d21d3 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 - 1 + 0 JellyfinClient.xcscheme_^#shared#^_ 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: "
") ?? [], 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() +//} diff --git a/Jel/Views/Library/Item/ItemGenresView.swift b/Jel/Views/Library/Item/ItemGenresView.swift deleted file mode 100644 index 4e8321f..0000000 --- a/Jel/Views/Library/Item/ItemGenresView.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// 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/Library/Item/ItemHeaderView.swift b/Jel/Views/Library/Item/ItemHeaderView.swift deleted file mode 100644 index 4c2bbe3..0000000 --- a/Jel/Views/Library/Item/ItemHeaderView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// 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/Library/Item/ItemInfoView.swift b/Jel/Views/Library/Item/ItemInfoView.swift deleted file mode 100644 index dda1c39..0000000 --- a/Jel/Views/Library/Item/ItemInfoView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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/Library/Item/ItemMediaView.swift b/Jel/Views/Library/Item/ItemMediaView.swift deleted file mode 100644 index 1d1e53d..0000000 --- a/Jel/Views/Library/Item/ItemMediaView.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// 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) - .padding(.bottom) - - ForEach(item.overview?.components(separatedBy: "
") ?? [], id: \.self) {overview in - Text(overview) - } - } - } -} - -//#Preview { -// ItemMovieView(item: BaseItemDto()) -//} diff --git a/Jel/Views/Library/Item/ItemView.swift b/Jel/Views/Library/Item/ItemView.swift deleted file mode 100644 index da85f32..0000000 --- a/Jel/Views/Library/Item/ItemView.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// 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/Library/Item/Person/ItemPeopleView.swift b/Jel/Views/Library/Item/Person/ItemPeopleView.swift deleted file mode 100644 index 6e2a974..0000000 --- a/Jel/Views/Library/Item/Person/ItemPeopleView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 deleted file mode 100644 index a6e5161..0000000 --- a/Jel/Views/Library/Item/Person/ItemPersonIconView.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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 deleted file mode 100644 index 5181e73..0000000 --- a/Jel/Views/Library/Item/Types/ItemMovieView.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// 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 < -100 - } - } - } - - ItemMediaView(item: item) - .padding() - - ItemGenresView(item: item) - - ItemPeopleView(item: item) - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(item.name ?? "Untitled") - .toolbarRole(.editor) - .toolbar { - ToolbarItem(placement: .principal) { - Text(pageScrolled ? item.name ?? "Untitled" : "") - .bold() - } - } - .toolbarBackground(pageScrolled ? .visible : .hidden) - } -} - -//#Preview { -// ItemMovieView() -//} diff --git a/Jel/Views/Library/ItemIconView.swift b/Jel/Views/Library/ItemIconView.swift deleted file mode 100644 index c2006cc..0000000 --- a/Jel/Views/Library/ItemIconView.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// 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/TODO.txt b/TODO.txt new file mode 100644 index 0000000..a2f41e4 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,10 @@ +=== +TODO + +- Implement play button + +=== +DONE + +- Use Color.seondaryLabel for gray text +- Fix text wrapping on ItemPersonIconView -- cgit v1.2.3