diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 5d110182..59089512 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892776263CBB000035E14B /* JellyApiTypings.swift */; }; 5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; }; 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; - 538CD954263E3DC100BB5AF0 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */; }; 53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; 53987CA626572F0700E7EA70 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; 53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; }; @@ -47,6 +46,8 @@ 621338932660107500A81A2A /* String++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String++.swift */; }; 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; + 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; + 621C638226676728004216EA /* LazyImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage++.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeaderScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */; }; 6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; }; 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; }; @@ -117,6 +118,7 @@ 621338922660107500A81A2A /* String++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String++.swift"; sourceTree = ""; }; 62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + 621C638126676728004216EA /* LazyImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LazyImage++.swift"; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderScrollView.swift; sourceTree = ""; }; 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPI.swift; sourceTree = ""; }; 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; @@ -130,13 +132,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 538CD954263E3DC100BB5AF0 /* SDWebImageSwiftUI in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */, 6273DD43265F4195009C1D0B /* Moya in Frameworks */, 53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */, 5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, + 621C638026672A30004216EA /* NukeUI in Frameworks */, 5302F82A2658791C00647A2E /* Sentry in Frameworks */, 5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */, ); @@ -221,6 +223,7 @@ 621338B22660A07800A81A2A /* LazyView.swift */, 621338922660107500A81A2A /* String++.swift */, 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */, + 621C638126676728004216EA /* LazyImage++.swift */, ); path = Extensions; sourceTree = ""; @@ -295,11 +298,11 @@ 5338F753263B65E10014BF09 /* SwiftyRequest */, 5338F756263B7E2E0014BF09 /* KeychainSwift */, 53892779263CBFE70035E14B /* SwiftyJSON */, - 538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */, 5302F8292658791C00647A2E /* Sentry */, 53352570265EA0A0006CCA86 /* Introspect */, 6273DD42265F4195009C1D0B /* Moya */, 6273DD44265F4195009C1D0B /* CombineMoya */, + 621C637F26672A30004216EA /* NukeUI */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */; @@ -335,10 +338,10 @@ 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */, 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */, 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - 538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, 5302F8282658791C00647A2E /* XCRemoteSwiftPackageReference "sentry-cocoa" */, 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */, + 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -369,6 +372,7 @@ buildActionMask = 2147483647; files = ( 621338932660107500A81A2A /* String++.swift in Sources */, + 621C638226676728004216EA /* LazyImage++.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, @@ -644,12 +648,12 @@ minimumVersion = 5.0.1; }; }; - 538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { + 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI"; + repositoryURL = "https://github.com/kean/NukeUI"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.2; + branch = main; + kind = branch; }; }; 6273DD41265F4195009C1D0B /* XCRemoteSwiftPackageReference "Moya" */ = { @@ -688,10 +692,10 @@ package = 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; - 538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */ = { + 621C637F26672A30004216EA /* NukeUI */ = { isa = XCSwiftPackageProductDependency; - package = 538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; - productName = SDWebImageSwiftUI; + package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; + productName = NukeUI; }; 6273DD42265F4195009C1D0B /* Moya */ = { isa = XCSwiftPackageProductDependency; diff --git a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45cf41c3..0793b3b3 100644 --- a/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,6 +28,15 @@ "version": "5.0.200" } }, + { + "package": "Gifu", + "repositoryURL": "https://github.com/kaishin/Gifu", + "state": { + "branch": null, + "revision": "0ffe24744cc3d82ab9edece53670d0352c6d5507", + "version": "3.3.0" + } + }, { "package": "KeychainSwift", "repositoryURL": "https://github.com/evgenyneu/keychain-swift", @@ -55,6 +64,24 @@ "version": "15.0.0-alpha.1" } }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "2775239e10e23c0b70c5544b98c2af7f65c2bbd9", + "version": "10.0.1" + } + }, + { + "package": "NukeUI", + "repositoryURL": "https://github.com/kean/NukeUI", + "state": { + "branch": "main", + "revision": "27dcb9065a18450ba47ecd46a913a74646d95144", + "version": null + } + }, { "package": "ReactiveSwift", "repositoryURL": "https://github.com/Moya/ReactiveSwift.git", @@ -73,24 +100,6 @@ "version": "5.1.2" } }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc", - "version": "5.11.1" - } - }, - { - "package": "SDWebImageSwiftUI", - "repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI", - "state": { - "branch": null, - "revision": "cd8625b7cf11a97698e180d28bb7d5d357196678", - "version": "2.0.2" - } - }, { "package": "Sentry", "repositoryURL": "https://github.com/getsentry/sentry-cocoa", diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 62a804a7..28495931 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -12,7 +12,7 @@ import SwiftyJSON import CoreData import KeychainSwift import Sentry -import SDWebImageSwiftUI +import NukeUI class publicUser: ObservableObject { @Published var username: String = ""; @@ -320,9 +320,8 @@ struct ConnectToServerView: View { Text(pubuser.username).font(.subheadline).fontWeight(.semibold) Spacer() if(pubuser.primaryImageTag != "") { - WebImage(url: URL(string: "\(uri)/Users/\(pubuser.id)/Images/Primary?width=200&quality=80&tag=\(pubuser.primaryImageTag)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .aspectRatio(contentMode: .fill) + LazyImage(source: URL(string: "\(uri)/Users/\(pubuser.id)/Images/Primary?width=200&quality=80&tag=\(pubuser.primaryImageTag)")) + .contentMode(.aspectFill) .frame(width: 60, height: 60) .cornerRadius(30.0) .shadow(radius: 6) diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 3a320a1f..93582613 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -8,10 +8,10 @@ import SwiftUI import KeychainSwift -import SDWebImageSwiftUI import Sentry import SwiftyJSON import SwiftyRequest +import Nuke struct ContentView: View { @Environment(\.managedObjectContext) @@ -75,12 +75,9 @@ struct ContentView: View { options.releaseName = "ios-" + (Bundle.main.infoDictionary?["CFBundleVersion"] as! String) options.enableOutOfMemoryTracking = true } - - let cache = SDImageCache(namespace: "tiny") - cache.config.maxMemoryCost = 125 * 1024 * 1024 // 125MB memory - cache.config.maxDiskSize = 1000 * 1024 * 1024 // 1000MB disk - SDImageCachesManager.shared.addCache(cache) - SDWebImageManager.defaultImageCache = SDImageCachesManager.shared + + ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory + DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk _libraries.wrappedValue = [] _library_names.wrappedValue = [:] diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 20bd9468..8b6be46c 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftyRequest import SwiftyJSON -import SDWebImageSwiftUI +import NukeUI struct CustomShape: Shape { let radius: CGFloat @@ -115,9 +115,8 @@ struct ContinueWatchingView: View { VStack(alignment: .leading) { Spacer().frame(height: 10) if(item.Type == "Episode") { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) .resizable() .frame(width: 320, height: 180) @@ -144,9 +143,8 @@ struct ContinueWatchingView: View { .padding(0), alignment: .bottomLeading ) } else { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) .resizable() .frame(width: 320, height: 180) diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 51db820f..5fb2a308 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -5,10 +5,10 @@ // Created by Aiden Vigue on 5/13/21. // -import SDWebImageSwiftUI import SwiftUI import SwiftyJSON import SwiftyRequest +import NukeUI struct EpisodeItemView: View { @EnvironmentObject @@ -203,29 +203,24 @@ struct EpisodeItemView: View { } var portraitHeaderView: some View { - VStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem .BackdropBlurHash, size: CGSize(width: 32, height: 32))!) .resizable() } - + .contentMode(.aspectFill) .opacity(0.3) - .aspectRatio(contentMode: .fill) .shadow(radius: 5) - } } var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash, @@ -233,7 +228,8 @@ struct EpisodeItemView: View { .resizable() .frame(width: 120, height: 180) .cornerRadius(10) - }.aspectRatio(contentMode: .fill) + } + .contentMode(.aspectFill) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -365,9 +361,8 @@ struct EpisodeItemView: View { ])), title: cast.Name) }) { VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: cast.Image) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: cast .ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -379,7 +374,8 @@ struct EpisodeItemView: View { .frame(width: 100, height: 100) .cornerRadius(10) } - .aspectRatio(contentMode: .fill) + + .contentMode(.aspectFill) .frame(width: 100, height: 100) .cornerRadius(10) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) @@ -424,9 +420,8 @@ struct EpisodeItemView: View { } else { GeometryReader { geometry in ZStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem .BackdropBlurHash, @@ -437,18 +432,17 @@ struct EpisodeItemView: View { height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets .bottom) } + .contentMode(.aspectFill) .opacity(0.3) - .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) .blur(radius: 2) HStack { VStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash, @@ -456,7 +450,7 @@ struct EpisodeItemView: View { .resizable() .frame(width: 120, height: 180) } - .aspectRatio(contentMode: .fill) + .contentMode(.fill) .frame(width: 120, height: 180) .cornerRadius(10) .shadow(radius: 5) @@ -585,9 +579,8 @@ struct EpisodeItemView: View { ])), title: cast.Name) }) { VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: cast.Image) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: cast .ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -599,7 +592,7 @@ struct EpisodeItemView: View { .frame(width: 100, height: 100) .cornerRadius(10) } - .aspectRatio(contentMode: .fill) + .contentMode(.aspectFill) .frame(width: 100, height: 100) .cornerRadius(10) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) diff --git a/JellyfinPlayer/Extensions/LazyImage++.swift b/JellyfinPlayer/Extensions/LazyImage++.swift new file mode 100644 index 00000000..d124f6a7 --- /dev/null +++ b/JellyfinPlayer/Extensions/LazyImage++.swift @@ -0,0 +1,22 @@ +// +// LazyImage++.swift +// JellyfinPlayer +// +// Created by PangMo5 on 2021/06/02. +// + +import Foundation +import SwiftUI +import NukeUI + +extension LazyImage { + func placeholderAndFailure(@ViewBuilder _ content: () -> Content?) -> LazyImage { + placeholder { + content() + } + .failure { + content() + } + } + +} diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 7170af52..88004fdc 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftyRequest import SwiftyJSON -import SDWebImageSwiftUI +import NukeUI struct LatestMediaView: View { @Environment(\.managedObjectContext) private var viewContext @@ -91,9 +91,8 @@ struct LatestMediaView: View { VStack(alignment: .leading) { if(item.Type == "Series") { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 100, height: 150) @@ -122,9 +121,8 @@ struct LatestMediaView: View { ) } else { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 100, height: 150) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index fd418a91..0fb7ae20 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -5,10 +5,10 @@ // Created by Aiden Vigue on 5/2/21. // -import SDWebImageSwiftUI import SwiftUI import SwiftyJSON import SwiftyRequest +import NukeUI struct LibrarySearchView: View { @Environment(\.managedObjectContext) @@ -68,7 +68,7 @@ struct LibrarySearchView: View { } } if viewModel.isLoading { - ActivityIndicator($viewModel.isLoading) + ProgressView() } else if viewModel.items.isEmpty { Text("Empty Response") } @@ -87,9 +87,8 @@ struct ResumeItemGridCell: View { var body: some View { VStack(alignment: .leading) { if item.Type == "Movie" { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) - .resizable() - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item .BlurHash, @@ -101,9 +100,8 @@ struct ResumeItemGridCell: View { .frame(width: 100, height: 150) .cornerRadius(10) } else { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) - .resizable() - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item .BlurHash, diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 454ee0a5..9823aeb0 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -5,8 +5,8 @@ // Created by Aiden Vigue on 5/1/21. // -import SDWebImageSwiftUI import SwiftUI +import NukeUI struct LibraryView: View { @Environment(\.managedObjectContext) @@ -87,7 +87,7 @@ struct LibraryView: View { recalcTracks() } if viewModel.isLoading { - ActivityIndicator($viewModel.isLoading) + ProgressView() } else if viewModel.items.isEmpty { Text("Empty Response") } @@ -136,9 +136,8 @@ extension LibraryView { var body: some View { VStack(alignment: .leading) { if item.Type == "Movie" { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .resizable() - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item .BlurHash, @@ -150,9 +149,8 @@ extension LibraryView { .frame(width: 100, height: 150) .cornerRadius(10) } else { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) - .resizable() - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: item .BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item .BlurHash, diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 969a4c40..452edfca 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -5,10 +5,10 @@ // Created by Aiden Vigue on 5/13/21. // -import SDWebImageSwiftUI import SwiftUI import SwiftyJSON import SwiftyRequest +import NukeUI class DetailItem: ObservableObject { @Published @@ -293,27 +293,24 @@ struct MovieItemView: View { } var portraitHeaderView: some View { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem .BackdropBlurHash, size: CGSize(width: 32, height: 32))!) .resizable() } - + .contentMode(.aspectFill) .opacity(0.3) - .aspectRatio(contentMode: .fill) .shadow(radius: 5) } var portraitHeaderOverlayView: some View { VStack(alignment: .leading) { HStack(alignment: .bottom, spacing: 12) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash, @@ -321,7 +318,8 @@ struct MovieItemView: View { .resizable() .frame(width: 120, height: 180) .cornerRadius(10) - }.aspectRatio(contentMode: .fill) + } + .contentMode(.aspectFill) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -454,9 +452,8 @@ struct MovieItemView: View { ])), title: cast.Name) }) { VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: cast.Image) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: cast .ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -468,7 +465,7 @@ struct MovieItemView: View { .frame(width: 100, height: 100) .cornerRadius(10) } - .aspectRatio(contentMode: .fill) + .contentMode(.aspectFill) .frame(width: 100, height: 100) .cornerRadius(10) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) @@ -513,9 +510,8 @@ struct MovieItemView: View { } else { GeometryReader { geometry in ZStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem .BackdropBlurHash, @@ -526,18 +522,17 @@ struct MovieItemView: View { height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets .bottom) } + .contentMode(.aspectFill) .opacity(0.3) - .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) .blur(radius: 2) HStack { VStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash, @@ -674,9 +669,8 @@ struct MovieItemView: View { ])), title: cast.Name) }) { VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: cast.Image) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: cast .ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : @@ -688,7 +682,7 @@ struct MovieItemView: View { .frame(width: 100, height: 100) .cornerRadius(10) } - .aspectRatio(contentMode: .fill) + .contentMode(.aspectFill) .frame(width: 100, height: 100) .cornerRadius(10) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 7b6e7a83..23e08323 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftyRequest import SwiftyJSON -import SDWebImageSwiftUI +import NukeUI struct NextUpView: View { @Environment(\.managedObjectContext) private var viewContext @@ -79,9 +79,8 @@ struct NextUpView: View { NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 100, height: 150) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 55ff0a5d..995a0d69 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -5,7 +5,7 @@ // Created by Aiden Vigue on 5/13/21. // -import SDWebImageSwiftUI +import NukeUI import SwiftUI import SwiftyJSON import SwiftyRequest @@ -18,7 +18,8 @@ struct SeasonItemView: View { @State private var isLoading: Bool = true var item: ResumeItem - var fullItem: DetailItem + @State + var fullItem = DetailItem() @State var episodes: [DetailItem] = [] @State @@ -28,7 +29,6 @@ struct SeasonItemView: View { init(item: ResumeItem) { self.item = item - self.fullItem = DetailItem() } func loadData() { @@ -48,32 +48,33 @@ struct SeasonItemView: View { let body = response.body do { let json = try JSON(data: body) - fullItem.ProductionYear = json["ProductionYear"].int ?? 0 - fullItem.Poster = json["ImageTags"]["Primary"].string ?? "" - fullItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][fullItem.Poster].string ?? "" - fullItem.Backdrop = json["BackdropImageTags"][0].string ?? "" - fullItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][fullItem.Backdrop].string ?? "" - fullItem.Name = json["Name"].string ?? "" - fullItem.Type = json["Type"].string ?? "" - fullItem.IndexNumber = json["IndexNumber"].int ?? nil - fullItem.SeriesId = json["ParentId"].string ?? nil - fullItem.Id = item.Id - fullItem.Overview = json["Overview"].string ?? "" - fullItem.Tagline = json["Taglines"][0].string ?? "" - fullItem.SeriesName = json["SeriesName"].string ?? nil - fullItem.ParentId = json["ParentId"].string ?? "" + let responseItem = DetailItem() + responseItem.ProductionYear = json["ProductionYear"].int ?? 0 + responseItem.Poster = json["ImageTags"]["Primary"].string ?? "" + responseItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][responseItem.Poster].string ?? "" + responseItem.Backdrop = json["BackdropImageTags"][0].string ?? "" + responseItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][responseItem.Backdrop].string ?? "" + responseItem.Name = json["Name"].string ?? "" + responseItem.Type = json["Type"].string ?? "" + responseItem.IndexNumber = json["IndexNumber"].int ?? nil + responseItem.SeriesId = json["ParentId"].string ?? nil + responseItem.Id = item.Id + responseItem.Overview = json["Overview"].string ?? "" + responseItem.Tagline = json["Taglines"][0].string ?? "" + responseItem.SeriesName = json["SeriesName"].string ?? nil + responseItem.ParentId = json["ParentId"].string ?? "" // People - fullItem.Directors = [] - fullItem.Studios = [] - fullItem.Writers = [] - fullItem.Cast = [] - fullItem.Genres = [] + responseItem.Directors = [] + responseItem.Studios = [] + responseItem.Writers = [] + responseItem.Cast = [] + responseItem.Genres = [] for (_, person): (String, JSON) in json["People"] { if person["Type"].stringValue == "Director" { - fullItem.Directors.append(person["Name"].string ?? "") + responseItem.Directors.append(person["Name"].string ?? "") } else if person["Type"].stringValue == "Writer" { - fullItem.Writers.append(person["Name"].string ?? "") + responseItem.Writers.append(person["Name"].string ?? "") } else if person["Type"].stringValue == "Actor" { let cast = CastMember() cast.Name = person["Name"].string ?? "" @@ -84,10 +85,12 @@ struct SeasonItemView: View { cast .Image = URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")! - fullItem.Cast.append(cast) + responseItem.Cast.append(cast) } } + _fullItem.wrappedValue = responseItem + let url2 = "/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user?.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview" let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url2) @@ -185,26 +188,29 @@ struct SeasonItemView: View { return result } + @ViewBuilder var portraitHeaderView: some View { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=750&quality=80&tag=\(item.SeasonImage ?? "")")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: item - .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .SeasonImageBlurHash ?? "", - size: CGSize(width: 32, height: 32))!) - .resizable() - } - .opacity(0.4) - .aspectRatio(contentMode: .fill) - .shadow(radius: 5) + if isLoading { + EmptyView() + } else { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")")) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item + .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item + .SeasonImageBlurHash ?? "", + size: CGSize(width: 32, height: 32))!) + .resizable() + } + .contentMode(.aspectFill) + .opacity(0.4) + .shadow(radius: 5) + } } var portraitHeaderOverlayView: some View { HStack(alignment: .bottom, spacing: 12) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: fullItem .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem .PosterBlurHash, @@ -212,7 +218,8 @@ struct SeasonItemView: View { .resizable() .frame(width: 120, height: 180) .cornerRadius(10) - }.aspectRatio(contentMode: .fill) + } + .contentMode(.aspectFill) .frame(width: 120, height: 180) .cornerRadius(10) VStack(alignment: .leading) { @@ -237,32 +244,156 @@ struct SeasonItemView: View { .padding(.bottom, -22) } - var body: some View { - VStack(alignment: .leading) { - LoadingView(isShowing: $isLoading) { - VStack(alignment: .leading) { - if orientationInfo.orientation == .portrait { - ParallaxHeaderScrollView(header: portraitHeaderView, - staticOverlayView: portraitHeaderOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIScreen.main.bounds.width * 0.5625) { - VStack(alignment: .leading) { - Spacer() - .frame(height: 22) + @ViewBuilder + var innerBody: some View { + if orientationInfo.orientation == .portrait { + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIScreen.main.bounds.width * 0.5625) { + LazyVStack(alignment: .leading) { + Spacer() + .frame(height: 22) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) + } + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + ForEach(episodes, id: \.Id) { episode in + NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { + HStack { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: episode + .PosterBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 150, height: 90) + .cornerRadius(10) + } + .contentMode(.aspectFill) + .shadow(radius: 5) + .frame(width: 150, height: 90) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10, style: .circular) + .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255) + .opacity(0.4)) + .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * + 150), + height: 90) + .padding(0), alignment: .bottomLeading) + VStack(alignment: .leading) { + HStack { + Text(episode.Name).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(1) + Spacer() + Text(episode.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + Spacer() + Text(episode.Overview).font(.footnote).foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true).lineLimit(4) + Spacer() + }.padding(.trailing, 20).offset(y: 2) + }.offset(x: 12, y: 0) + } + } + if !fullItem.Directors.isEmpty { + HStack { + Text("Directors:").font(.callout).fontWeight(.semibold) + Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) + } + if !fullItem.Writers.isEmpty { + HStack { + Text("Writers:").font(.callout).fontWeight(.semibold) + Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) + } + if !fullItem.Studios.isEmpty { + HStack { + Text("Studios:").font(.callout).fontWeight(.semibold) + Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) + .foregroundColor(Color.secondary) + }.padding(.leading, 16).padding(.trailing, 16) + } + Spacer().frame(height: 3) + } + } + } else { + GeometryReader { geometry in + ZStack { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")")) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: item + .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item + .SeasonImageBlurHash ?? "", + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets + .trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets + .bottom) + } + .contentMode(.aspectFill) + + .opacity(0.4) + .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, + height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) + .edgesIgnoringSafeArea(.all) + .blur(radius: 2) + HStack { + VStack(alignment: .leading) { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + .placeholderAndFailure { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + } + .contentMode(.aspectFill) + .frame(width: 120, height: 180) + .cornerRadius(10) + Spacer().frame(height: 4) + if fullItem.ProductionYear != 0 { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + } + Spacer() + } + ScrollView { + LazyVStack(alignment: .leading) { if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) .padding(.trailing, 16) } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) + if fullItem.Overview != "" { + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + } ForEach(episodes, id: \.Id) { episode in NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { HStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: episode .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem @@ -271,7 +402,8 @@ struct SeasonItemView: View { .resizable() .frame(width: 150, height: 90) .cornerRadius(10) - }.aspectRatio(contentMode: .fill) + } + .contentMode(.aspectFill) .shadow(radius: 5) .frame(width: 150, height: 90) .cornerRadius(10) @@ -289,11 +421,30 @@ struct SeasonItemView: View { .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .lineLimit(1) - Spacer() Text(episode.Runtime).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) + if episode.OfficialRating != "" { + Text(episode.OfficialRating).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if episode.CommunityRating != "" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(episode.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -6, y: 0) + } + } + Spacer() } Spacer() Text(episode.Overview).font(.footnote).foregroundColor(.secondary) @@ -324,163 +475,19 @@ struct SeasonItemView: View { .foregroundColor(Color.secondary) }.padding(.leading, 16).padding(.trailing, 16) } - Spacer().frame(height: 3) - } - } - } else { - GeometryReader { geometry in - ZStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: item - .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .SeasonImageBlurHash ?? "", - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing, - height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets - .bottom) - } - - .opacity(0.4) - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, - height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) - .edgesIgnoringSafeArea(.all) - .blur(radius: 2) - HStack { - VStack(alignment: .leading) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - Spacer().frame(height: 4) - if fullItem.ProductionYear != 0 { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - } - Spacer() - } - ScrollView { - VStack(alignment: .leading) { - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - if fullItem.Overview != "" { - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - } - ForEach(episodes, id: \.Id) { episode in - NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { - HStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: episode - .PosterBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .PosterBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 150, height: 90) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .shadow(radius: 5) - .frame(width: 150, height: 90) - .cornerRadius(10) - .overlay(RoundedRectangle(cornerRadius: 10, style: .circular) - .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255) - .opacity(0.4)) - .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * - 150), - height: 90) - .padding(0), alignment: .bottomLeading) - VStack(alignment: .leading) { - HStack { - Text(episode.Name).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(1) - Text(episode.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if episode.OfficialRating != "" { - Text(episode.OfficialRating).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } - if episode.CommunityRating != "" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(episode.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -6, y: 0) - } - } - Spacer() - } - Spacer() - Text(episode.Overview).font(.footnote).foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true).lineLimit(4) - Spacer() - }.padding(.trailing, 20).offset(y: 2) - }.offset(x: 12, y: 0) - } - } - if !fullItem.Directors.isEmpty { - HStack { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Writers.isEmpty { - HStack { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Studios.isEmpty { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - Spacer().frame(height: 125) - }.frame(maxHeight: .infinity) - }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 0) - } - } - } + Spacer().frame(height: 125) + }.frame(maxHeight: .infinity) + }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 0) } } } + } + + var body: some View { + LoadingView(isShowing: $isLoading) { + innerBody + } .onAppear(perform: loadData) .navigationBarTitleDisplayMode(.inline) .navigationTitle("\(item.Name) - \(item.SeriesName ?? "")") diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 279959ab..9689cf32 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftyRequest import SwiftyJSON -import SDWebImageSwiftUI +import NukeUI struct SeriesItemView: View { @EnvironmentObject var globalData: GlobalData @@ -96,9 +96,8 @@ struct SeriesItemView: View { ForEach(items, id: \.Id) { item in NavigationLink(destination: ItemView(item: item )) { VStack(alignment: .leading) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)")) - .resizable() - .placeholder { + LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)")) + .placeholderAndFailure { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 100, height: 150)