From 69bed7745833add72cfe1b1516a61f62460b1765 Mon Sep 17 00:00:00 2001 From: Shav Kinderlehrer Date: Sun, 24 Dec 2023 20:01:52 -0500 Subject: [PATCH] Implement movieDetailView --- Jel.xcodeproj/project.pbxproj | 24 ++++++ Jel/Models/JellyfinKitExtensions.swift | 28 +++++++ Jel/Models/ViewOffsetKey.swift | 17 +++++ Jel/Views/Library/Item/ItemHeaderView.swift | 42 ++++++----- Jel/Views/Library/Item/ItemInfoView.swift | 29 ++++++++ Jel/Views/Library/Item/ItemMovieView.swift | 82 ++++++++++----------- Jel/Views/Library/LibraryDetailView.swift | 1 + Jel/Views/Library/LibraryIconView.swift | 2 - Jel/Views/Utility/StickyHeaderView.swift | 38 ++++++++++ 9 files changed, 199 insertions(+), 64 deletions(-) create mode 100644 Jel/Models/JellyfinKitExtensions.swift create mode 100644 Jel/Models/ViewOffsetKey.swift create mode 100644 Jel/Views/Library/Item/ItemInfoView.swift create mode 100644 Jel/Views/Utility/StickyHeaderView.swift diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj index d427a34..be0a680 100644 --- a/Jel.xcodeproj/project.pbxproj +++ b/Jel.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 3D13F95F2B375DB800E91913 /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13F95E2B375DB800E91913 /* ItemView.swift */; }; 3D13F9612B37637500E91913 /* ItemMovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13F9602B37637500E91913 /* ItemMovieView.swift */; }; 3D13F9652B37EC7A00E91913 /* ItemHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13F9642B37EC7A00E91913 /* ItemHeaderView.swift */; }; + 3D13F9692B389FA300E91913 /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13F9682B389FA300E91913 /* ViewOffsetKey.swift */; }; + 3D13F96F2B38A32500E91913 /* StickyHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D13F96E2B38A32500E91913 /* StickyHeaderView.swift */; }; 3D16FC3C2B2CDFB500E6D8B3 /* DashboardLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D16FC3B2B2CDFB500E6D8B3 /* DashboardLibraryView.swift */; }; 3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */; }; 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */; }; @@ -32,6 +34,8 @@ 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDC82B28C62800919017 /* SignInView.swift */; }; 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */; }; 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 */; }; 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 */; }; @@ -75,6 +79,8 @@ 3D13F95E2B375DB800E91913 /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 3D13F9602B37637500E91913 /* ItemMovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemMovieView.swift; sourceTree = ""; }; 3D13F9642B37EC7A00E91913 /* ItemHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemHeaderView.swift; sourceTree = ""; }; + 3D13F9682B389FA300E91913 /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = ""; }; + 3D13F96E2B38A32500E91913 /* StickyHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyHeaderView.swift; sourceTree = ""; }; 3D16FC3B2B2CDFB500E6D8B3 /* DashboardLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardLibraryView.swift; sourceTree = ""; }; 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = ""; }; 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryIconView.swift; sourceTree = ""; }; @@ -94,6 +100,8 @@ 3D91FDC82B28C62800919017 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInToServerView.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -134,6 +142,7 @@ 3D1015D72B27F54A00F5C29A /* Views */ = { isa = PBXGroup; children = ( + 3D13F96D2B38A31300E91913 /* Utility */, 3D9063CC2B279A310063DD2A /* ContentView.swift */, 3DDD67902B293B780026781E /* Dashboard */, 3D8AB2A62B366309005BD7D0 /* Library */, @@ -158,6 +167,8 @@ children = ( 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */, 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */, + 3D13F9682B389FA300E91913 /* ViewOffsetKey.swift */, + 3DAFA8E92B39039900D71AD1 /* JellyfinKitExtensions.swift */, ); path = Models; sourceTree = ""; @@ -167,11 +178,20 @@ children = ( 3D13F95E2B375DB800E91913 /* ItemView.swift */, 3D13F9602B37637500E91913 /* ItemMovieView.swift */, + 3DAFA8E72B38AFED00D71AD1 /* ItemInfoView.swift */, 3D13F9642B37EC7A00E91913 /* ItemHeaderView.swift */, ); path = Item; sourceTree = ""; }; + 3D13F96D2B38A31300E91913 /* Utility */ = { + isa = PBXGroup; + children = ( + 3D13F96E2B38A32500E91913 /* StickyHeaderView.swift */, + ); + path = Utility; + sourceTree = ""; + }; 3D8AB2A62B366309005BD7D0 /* Library */ = { isa = PBXGroup; children = ( @@ -414,10 +434,13 @@ buildActionMask = 2147483647; files = ( 3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */, + 3D13F96F2B38A32500E91913 /* StickyHeaderView.swift in Sources */, 3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */, 3D1015D92B27F57400F5C29A /* AddServerView.swift in Sources */, + 3DAFA8EA2B39039900D71AD1 /* JellyfinKitExtensions.swift in Sources */, 3D13F9652B37EC7A00E91913 /* ItemHeaderView.swift in Sources */, 3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */, + 3D13F9692B389FA300E91913 /* ViewOffsetKey.swift in Sources */, 3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */, 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */, 3D8AB2A82B366353005BD7D0 /* LibraryDetailView.swift in Sources */, @@ -426,6 +449,7 @@ 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */, 3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */, 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */, + 3DAFA8E82B38AFED00D71AD1 /* ItemInfoView.swift in Sources */, 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */, 3D16FC3C2B2CDFB500E6D8B3 /* DashboardLibraryView.swift in Sources */, 3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */, diff --git a/Jel/Models/JellyfinKitExtensions.swift b/Jel/Models/JellyfinKitExtensions.swift new file mode 100644 index 0000000..197731c --- /dev/null +++ b/Jel/Models/JellyfinKitExtensions.swift @@ -0,0 +1,28 @@ +// +// JellyfinKitExtensions.swift +// Jel +// +// Created by zerocool on 12/24/23. +// + +import Foundation +import JellyfinKit + +extension BaseItemDto { + func getRuntime() -> String? { + let formatter: DateComponentsFormatter = { + let localFormatter = DateComponentsFormatter() + localFormatter.unitsStyle = .brief + localFormatter.allowedUnits = [.hour, .minute] + + return localFormatter + }() + + if let runTimeTicks = self.runTimeTicks { + let text = formatter.string(from: Double(runTimeTicks / 10_000_000)) + return text + } + + return nil + } +} diff --git a/Jel/Models/ViewOffsetKey.swift b/Jel/Models/ViewOffsetKey.swift new file mode 100644 index 0000000..b118478 --- /dev/null +++ b/Jel/Models/ViewOffsetKey.swift @@ -0,0 +1,17 @@ +// +// ViewOffsetKey.swift +// Jel +// +// Created by zerocool on 12/24/23. +// + +import SwiftUI + +/// A preference key to store ScrollView offset +public struct ViewOffsetKey: PreferenceKey { + public typealias Value = CGFloat + public static var defaultValue = CGFloat.zero + public static func reduce(value: inout Value, nextValue: () -> Value) { + value += nextValue() + } +} diff --git a/Jel/Views/Library/Item/ItemHeaderView.swift b/Jel/Views/Library/Item/ItemHeaderView.swift index d694c13..2c11719 100644 --- a/Jel/Views/Library/Item/ItemHeaderView.swift +++ b/Jel/Views/Library/Item/ItemHeaderView.swift @@ -13,35 +13,43 @@ struct ItemHeaderView: View { let overlayGradient = LinearGradient(gradient: Gradient(stops: [ .init(color: .clear, location: 0), - .init(color: .black, location: 0.3), - .init(color: .black, location: 0.7), - - .init(color: .clear, location: 1) + .init(color: .black, location: 0.5), + // .init(color: .black, location: 0.7), + // .init(color: .clear, location: 1) ]), startPoint: .bottom, endPoint: .top) var body: some View { ZStack(alignment: .bottom) { - LibraryIconView(library: item, imageType: "Backdrop", contentMode: .fill) - .hideCaption() - .setCornerRadius(0) - .mask(overlayGradient) -// .padding(.top, 50) - .background { - LibraryIconView(library: item, imageType: "Backdrop", contentMode: .fill) - .hideCaption() - .setCornerRadius(0) - .blur(radius: 50) - } + StickyHeaderView(minHeight: 300) { + LibraryIconView(library: item, imageType: "Backdrop", contentMode: .fill) + .hideCaption() + .setCornerRadius(0) + .mask(overlayGradient) + .background { + LibraryIconView(library: item, imageType: "Backdrop", contentMode: .fill) + .hideCaption() + .setCornerRadius(0) + .blur(radius: 50) + } + } HStack { LibraryIconView(library: item, imageType: "Logo", width: 200, height: 100, placeHolder: AnyView(Text(item.name ?? "Unknown").font(.title).bold().truncationMode(.middle))) .hideCaption() .setCornerRadius(0) .shadow(radius: 10) + .frame(alignment: .leading) Spacer() + ItemInfoView(item: item) + .foregroundStyle(.white) } - .frame(alignment: .leading) - .padding(.leading) + .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) } } } diff --git a/Jel/Views/Library/Item/ItemInfoView.swift b/Jel/Views/Library/Item/ItemInfoView.swift new file mode 100644 index 0000000..d48dfef --- /dev/null +++ b/Jel/Views/Library/Item/ItemInfoView.swift @@ -0,0 +1,29 @@ +// +// ItemInfoView.swift +// Jel +// +// Created by zerocool on 12/24/23. +// + +import SwiftUI +import JellyfinKit + +struct ItemInfoView: View { + @State var item: BaseItemDto + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(item.genres?.first ?? "---") + Text("•") + Text((item.productionYear != nil) ? String(item.productionYear!) : "---") + } + Text(item.getRuntime() ?? "-:--") + } + .font(.caption) + } +} + +//#Preview { +// ItemInfoView() +//} diff --git a/Jel/Views/Library/Item/ItemMovieView.swift b/Jel/Views/Library/Item/ItemMovieView.swift index 68ba1b2..be12696 100644 --- a/Jel/Views/Library/Item/ItemMovieView.swift +++ b/Jel/Views/Library/Item/ItemMovieView.swift @@ -18,59 +18,50 @@ struct ItemMovieView: View { @State var navigationTitle: String = "" var body: some View { - ScrollView { - // ItemHeaderView(item: item) - // .scrollTransition {content, phase in - // content - // .scaleEffect(phase.isIdentity ? 1 : 2) - // .opacity(phase.isIdentity ? 1 : 0.1) - // .blur(radius: phase.isIdentity ? 0 : 50) - // } - ItemHeaderView(item: item) - .opacity(0) // this is the jankiest thing in existence - .background { - GeometryReader {geo in - ItemHeaderView(item: item) - .onChange(of: geo.frame(in: .global).minY) { - navigationTitle = geo.frame(in: .global).minY < 0 ? item.name ?? "Unknown" : "" - } - .scaleEffect(1 + (geo.frame(in: .global).minY > 0 ? geo.frame(in: .global).minY * 0.001 : 0)) - .offset(y: 1 + (geo.frame(in: .global).minY > 0 ? geo.frame(in: .global).minY * 0.001 : 0)) - .scrollTransition {content, phase in - content - .scaleEffect(phase.isIdentity ? 1 : 2) - .opacity(phase.isIdentity ? 1 : 0.1) - .blur(radius: phase.isIdentity ? 0 : 50) + VStack { + if loading { + ProgressView() + .progressViewStyle(.circular) + } else { + ScrollView { + ItemHeaderView(item: item) + .padding(.bottom) + .background { + GeometryReader {geo in + EmptyView() + .onChange(of: geo.frame(in: .global).minY) { + let minY = geo.frame(in: .global).minY + if minY < 0 { + navigationTitle = item.name ?? "" + } else { + navigationTitle = "" + } + } } + } + + VStack(alignment: .leading) { + Text(item.taglines?.count ?? 0 > 0 ? item.taglines?[0] ?? "" : "") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(item.overview ?? "---") } + .padding() } - - VStack { - Text(item.taglines?[0] ?? "Unknown") - .font(.headline) - .padding(.top, 20) - - Text(item.overview ?? "Unknown") - .padding() - Text(item.overview ?? "Unknown") - .padding() - Text(item.overview ?? "Unknown") - .padding() - Text(item.overview ?? "Unknown") - .padding() } } - .redacted(reason: loading ? .placeholder : []) - .ignoresSafeArea(edges: .top) - .scrollIndicators(.hidden) .toolbarRole(.editor) - .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) + .navigationTitle(navigationTitle) + .ignoresSafeArea(edges: .bottom) + .scrollIndicators(.hidden) .onAppear { Task { do { let request = Paths.getItem(userID: authState.userId ?? "", itemID: item.id ?? "") - item = try await jellyfinClient.send(request).value + let response = try await jellyfinClient.send(request) + item = response.value loading = false } catch { } @@ -79,6 +70,7 @@ struct ItemMovieView: View { } } -#Preview { - ItemMovieView(item: BaseItemDto()) -} +//#Preview { +// ItemMovieView(item: BaseItemDto()) + +//} diff --git a/Jel/Views/Library/LibraryDetailView.swift b/Jel/Views/Library/LibraryDetailView.swift index 4b140df..f4ff93c 100644 --- a/Jel/Views/Library/LibraryDetailView.swift +++ b/Jel/Views/Library/LibraryDetailView.swift @@ -47,6 +47,7 @@ struct LibraryDetailView: View { do { let res = try await jellyfinClient.send(request) items = res.value.items + items?.sort(by: {$0.name?.lowercased() ?? "" < $1.name?.lowercased() ?? ""}) loading = false } catch { } diff --git a/Jel/Views/Library/LibraryIconView.swift b/Jel/Views/Library/LibraryIconView.swift index a3f5b55..a849446 100644 --- a/Jel/Views/Library/LibraryIconView.swift +++ b/Jel/Views/Library/LibraryIconView.swift @@ -32,8 +32,6 @@ struct LibraryIconView: View { if let image = state.image { image .resizable() - } else if state.error != nil { - Color.red } else { if let content = placeHolder { content diff --git a/Jel/Views/Utility/StickyHeaderView.swift b/Jel/Views/Utility/StickyHeaderView.swift new file mode 100644 index 0000000..52dc26d --- /dev/null +++ b/Jel/Views/Utility/StickyHeaderView.swift @@ -0,0 +1,38 @@ +// +// StickyHeaderView.swift +// Jel +// +// Created by zerocool on 12/24/23. +// + +import SwiftUI + +struct StickyHeaderView: View { + + var minHeight: CGFloat + var content: Content + + init(minHeight: CGFloat = 200, @ViewBuilder content: () -> Content) { + self.minHeight = minHeight + self.content = content() + } + + var body: some View { + GeometryReader { geo in + if(geo.frame(in: .global).minY <= 0) { + content + .frame(width: geo.size.width, height: geo.size.height, alignment: .center) + } else { + content + .offset(y: -geo.frame(in: .global).minY) + .frame(width: geo.size.width, height: geo.size.height + geo.frame(in: .global).minY) + } + }.frame(minHeight: minHeight) + } +} + +#Preview { + StickyHeaderView { + Text("Test") + } +}