diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index eec8530e..54cc4ac8 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -199,6 +199,9 @@ 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; + 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; + 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; + 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -534,6 +537,7 @@ 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = ""; }; 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = ""; }; + 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; @@ -1059,6 +1063,7 @@ isa = PBXGroup; children = ( 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, + 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, @@ -1818,6 +1823,7 @@ E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, + 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, @@ -1977,6 +1983,7 @@ 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, + 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */, C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, @@ -2025,6 +2032,7 @@ E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */, E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */, + 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */, diff --git a/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift index 85ca7248..c805c8bc 100644 --- a/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift +++ b/JellyfinPlayer/Views/ItemView/Landscape/ItemLandscapeMainView.swift @@ -84,22 +84,24 @@ struct ItemLandscapeMainView: View { // MARK: body var body: some View { - ZStack { - // MARK: Backdrop - - ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), - bh: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.3) - .edgesIgnoringSafeArea(.all) - .blur(radius: 4) - .layoutPriority(-1) - - // iPadOS is making the view go all the way to the edge. - // We have to accomodate this here - if UIDevice.current.userInterfaceIdiom == .pad { - innerBody.padding(.horizontal, 25) - } else { - innerBody + VStack { + ZStack { + // MARK: Backdrop + + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), + bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.3) + .edgesIgnoringSafeArea(.all) + .blur(radius: 8) + .layoutPriority(-1) + + // iPadOS is making the view go all the way to the edge. + // We have to accomodate this here + if UIDevice.current.userInterfaceIdiom == .pad { + innerBody.padding(.horizontal, 25) + } else { + innerBody + } } } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 95036a2f..2f27801d 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -1,11 +1,11 @@ // - /* - * SwiftFin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ import Foundation import JellyfinAPI @@ -14,35 +14,48 @@ import UIKit // 001fC^ = dark grey plain blurhash public extension BaseItemDto { - // MARK: Images func getSeriesBackdropImageBlurHash() -> String { - let rawImgURL = getSeriesBackdropImage(maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + let imgURL = getSeriesBackdropImage(maxWidth: 1) + guard let imgTag = imgURL.queryParameters?["tag"], + let hash = imageBlurHashes?.backdrop?[imgTag] + else { + return "001fC^" + } - return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" + return hash } func getSeriesPrimaryImageBlurHash() -> String { - let rawImgURL = getSeriesPrimaryImage(maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + let imgURL = getSeriesPrimaryImage(maxWidth: 1) + guard let imgTag = imgURL.queryParameters?["tag"], + let hash = imageBlurHashes?.primary?[imgTag] + else { + return "001fC^" + } - return imageBlurHashes?.primary?[imgTag] ?? "001fC^" + return hash } func getPrimaryImageBlurHash() -> String { - let rawImgURL = getPrimaryImage(maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + let imgURL = getPrimaryImage(maxWidth: 1) + guard let imgTag = imgURL.queryParameters?["tag"], + let hash = imageBlurHashes?.primary?[imgTag] + else { + return "001fC^" + } - return imageBlurHashes?.primary?[imgTag] ?? "001fC^" + return hash } func getBackdropImageBlurHash() -> String { - let rawImgURL = getBackdropImage(maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + let imgURL = getBackdropImage(maxWidth: 1) + guard let imgTag = imgURL.queryParameters?["tag"] else { + return "001fC^" + } - if rawImgURL.contains("Backdrop") { + if imgURL.queryParameters?[ImageType.backdrop.rawValue] == nil { return imageBlurHashes?.backdrop?[imgTag] ?? "001fC^" } else { return imageBlurHashes?.primary?[imgTag] ?? "001fC^" @@ -50,31 +63,33 @@ public extension BaseItemDto { } func getBackdropImage(maxWidth: Int) -> URL { - var imageType = "" - var imageTag = "" + var imageType = ImageType.backdrop + var imageTag: String? var imageItemId = id ?? "" if primaryImageAspectRatio ?? 0.0 < 1.0 { - imageType = "Backdrop" if !(backdropImageTags?.isEmpty ?? true) { - imageTag = (backdropImageTags ?? [""])[0] + imageTag = backdropImageTags?.first } } else { - imageType = "Primary" - imageTag = imageTags?["Primary"] ?? "" + imageType = .primary + imageTag = imageTags?[ImageType.primary.rawValue] ?? "" } - if imageTag == "" || imageItemId == "" { - imageType = "Backdrop" + if imageTag == nil || imageItemId.isEmpty { if !(parentBackdropImageTags?.isEmpty ?? true) { - imageTag = (parentBackdropImageTags ?? [""])[0] + imageTag = parentBackdropImageTags?.first imageItemId = parentBackdropItemId ?? "" } } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, + imageType: imageType, + maxWidth: Int(x), + quality: 96, + tag: imageTag).URLString return URL(string: urlString)! } @@ -86,39 +101,42 @@ public extension BaseItemDto { } func getSeriesBackdropImage(maxWidth: Int) -> URL { - let imageType = "Backdrop" - let imageTag = (parentBackdropImageTags ?? [""])[0] - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: parentBackdropItemId ?? "", + imageType: .backdrop, + maxWidth: Int(x), + quality: 96, + tag: parentBackdropImageTags?.first).URLString return URL(string: urlString)! } func getSeriesPrimaryImage(maxWidth: Int) -> URL { - let imageType = "Primary" - let imageTag = seriesPrimaryImageTag ?? "" let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: seriesId ?? "", + imageType: .primary, + maxWidth: Int(x), + quality: 96, + tag: seriesPrimaryImageTag).URLString return URL(string: urlString)! } func getPrimaryImage(maxWidth: Int) -> URL { - let imageType = "Primary" - var imageTag = imageTags?["Primary"] ?? "" + let imageType = ImageType.primary + var imageTag = imageTags?[ImageType.primary.rawValue] ?? "" var imageItemId = id ?? "" - if imageTag == "" || imageItemId == "" { + if imageTag.isEmpty || imageItemId.isEmpty { imageTag = seriesPrimaryImageTag ?? "" imageItemId = seriesId ?? "" } let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - let urlString = - "\(SessionManager.main.currentLogin.server.uri)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=96&tag=\(imageTag)" - // print(urlString) + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: imageItemId, + imageType: imageType, + maxWidth: Int(x), + quality: 96, + tag: imageTag).URLString return URL(string: urlString)! } @@ -174,14 +192,14 @@ public extension BaseItemDto { } var itemType: ItemType { - guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown } + guard let originalType = type, let knownType = ItemType(rawValue: originalType) else { return .unknown } return knownType } // MARK: PortraitHeaderViewURL func portraitHeaderViewURL(maxWidth: Int) -> URL { - switch self.itemType { + switch itemType { case .movie, .season, .series: return getPrimaryImage(maxWidth: maxWidth) case .episode: diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index 007ed999..4d307df0 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -10,23 +10,28 @@ import JellyfinAPI import UIKit extension BaseItemPerson { - + // MARK: Get Image func getImage(baseURL: String, maxWidth: Int) -> URL { - let imageType = "Primary" - let imageTag = primaryImageTag ?? "" - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = "\(baseURL)/Items/\(id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "", + imageType: .primary, + maxWidth: Int(x), + quality: 96, + tag: primaryImageTag).URLString return URL(string: urlString)! } - + func getBlurHash() -> String { - let rawImgURL = getImage(baseURL: "", maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + let imgURL = getImage(baseURL: "", maxWidth: 1) + guard let imgTag = imgURL.queryParameters?["tag"], + let hash = imageBlurHashes?.primary?[imgTag] + else { + return "001fC^" + } - return imageBlurHashes?.primary?[imgTag] ?? "001fC^" + return hash } // MARK: First Role diff --git a/Shared/Extensions/URLExtensions.swift b/Shared/Extensions/URLExtensions.swift new file mode 100644 index 00000000..ec7ea5d2 --- /dev/null +++ b/Shared/Extensions/URLExtensions.swift @@ -0,0 +1,26 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation + +public extension URL { + /// Dictionary of the URL's query parameters + var queryParameters: [String: String]? { + guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), + let queryItems = components.queryItems else { return nil } + + var items: [String: String] = [:] + + for queryItem in queryItems { + items[queryItem.name] = queryItem.value + } + + return items + } +} diff --git a/Translations/cs.lproj/Localizable.strings b/Translations/cs.lproj/Localizable.strings index 5394ced2..17818a99 100644 Binary files a/Translations/cs.lproj/Localizable.strings and b/Translations/cs.lproj/Localizable.strings differ diff --git a/Translations/eo.lproj/Localizable.strings b/Translations/eo.lproj/Localizable.strings index 8561f8d7..e3aaaec5 100644 Binary files a/Translations/eo.lproj/Localizable.strings and b/Translations/eo.lproj/Localizable.strings differ diff --git a/Translations/id.lproj/Localizable.strings b/Translations/id.lproj/Localizable.strings new file mode 100644 index 00000000..ed60b89c Binary files /dev/null and b/Translations/id.lproj/Localizable.strings differ diff --git a/Translations/ta.lproj/Localizable.strings b/Translations/ta.lproj/Localizable.strings index e69de29b..6c5fac38 100644 Binary files a/Translations/ta.lproj/Localizable.strings and b/Translations/ta.lproj/Localizable.strings differ diff --git a/Translations/zh-Hant.lproj/Localizable.strings b/Translations/zh-Hant.lproj/Localizable.strings index dbfd1185..713548a5 100644 Binary files a/Translations/zh-Hant.lproj/Localizable.strings and b/Translations/zh-Hant.lproj/Localizable.strings differ