From 568b4d6126e1408e8e16417f22370a4ced44d105 Mon Sep 17 00:00:00 2001 From: Shav Kinderlehrer Date: Fri, 22 Dec 2023 19:29:07 -0500 Subject: [PATCH] Implement library view --- Jel.xcodeproj/project.pbxproj | 21 +-- .../xcshareddata/swiftpm/Package.resolved | 9 -- Jel/Models/BlurHashDecode.swift | 153 ++++++++++++++++++ .../Dashboard/Library/LibraryIconView.swift | 10 +- Jel/Views/Dashboard/Library/LibraryView.swift | 2 +- Jel/Views/Utility/AsyncImageView.swift | 7 +- 6 files changed, 170 insertions(+), 32 deletions(-) create mode 100644 Jel/Models/BlurHashDecode.swift diff --git a/Jel.xcodeproj/project.pbxproj b/Jel.xcodeproj/project.pbxproj index 374ee5d..cd85c84 100644 --- a/Jel.xcodeproj/project.pbxproj +++ b/Jel.xcodeproj/project.pbxproj @@ -11,12 +11,12 @@ 3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */; }; 3D1015DE2B27F79900F5C29A /* DatamodelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */; }; 3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015E32B28000E00F5C29A /* AuthStateController.swift */; }; - 3D16FC3A2B2CC22A00E6D8B3 /* BlurHashKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D16FC392B2CC22A00E6D8B3 /* BlurHashKit */; }; 3D16FC3C2B2CDFB500E6D8B3 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D16FC3B2B2CDFB500E6D8B3 /* LibraryView.swift */; }; 3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */; }; 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */; }; 3D7709392B29139700199889 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7709382B29139700199889 /* Pulse */; }; 3D77093B2B29139700199889 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3D77093A2B29139700199889 /* PulseUI */; }; + 3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */; }; 3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CA2B279A310063DD2A /* JelApp.swift */; }; 3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CC2B279A310063DD2A /* ContentView.swift */; }; 3D9063CF2B279A320063DD2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D9063CE2B279A320063DD2A /* Assets.xcassets */; }; @@ -74,6 +74,7 @@ 3D16FC3B2B2CDFB500E6D8B3 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.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 = ""; }; + 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 3D9063C72B279A310063DD2A /* Jel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3D9063CA2B279A310063DD2A /* JelApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelApp.swift; sourceTree = ""; }; 3D9063CC2B279A310063DD2A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -101,7 +102,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3D16FC3A2B2CC22A00E6D8B3 /* BlurHashKit in Frameworks */, 3D77093B2B29139700199889 /* PulseUI in Frameworks */, 3D7709392B29139700199889 /* Pulse in Frameworks */, 3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */, @@ -152,6 +152,7 @@ isa = PBXGroup; children = ( 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */, + 3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */, ); path = Models; sourceTree = ""; @@ -284,7 +285,6 @@ 3D9064582B27E4C70063DD2A /* JellyfinKit */, 3D7709382B29139700199889 /* Pulse */, 3D77093A2B29139700199889 /* PulseUI */, - 3D16FC392B2CC22A00E6D8B3 /* BlurHashKit */, ); productName = Jel; productReference = 3D9063C72B279A310063DD2A /* Jel.app */; @@ -360,7 +360,6 @@ mainGroup = 3D9063BE2B279A310063DD2A; packageReferences = ( 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */, - 3D16FC382B2CC22A00E6D8B3 /* XCRemoteSwiftPackageReference "BlurHashKit" */, ); productRefGroup = 3D9063C82B279A310063DD2A /* Products */; projectDirPath = ""; @@ -414,6 +413,7 @@ 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */, 3DDD67932B293BC40026781E /* DashboardView.swift in Sources */, 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */, + 3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */, 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */, 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */, 3D16FC3C2B2CDFB500E6D8B3 /* LibraryView.swift in Sources */, @@ -785,14 +785,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 3D16FC382B2CC22A00E6D8B3 /* XCRemoteSwiftPackageReference "BlurHashKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/LePips/BlurHashKit"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 1.2.0; - }; - }; 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/Pulse"; @@ -804,11 +796,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 3D16FC392B2CC22A00E6D8B3 /* BlurHashKit */ = { - isa = XCSwiftPackageProductDependency; - package = 3D16FC382B2CC22A00E6D8B3 /* XCRemoteSwiftPackageReference "BlurHashKit" */; - productName = BlurHashKit; - }; 3D7709382B29139700199889 /* Pulse */ = { isa = XCSwiftPackageProductDependency; package = 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */; diff --git a/Jel.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Jel.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71669bf..e1e75ae 100644 --- a/Jel.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Jel.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "BlurHashKit", - "repositoryURL": "https://github.com/LePips/BlurHashKit", - "state": { - "branch": null, - "revision": "c0bd7423398de68cbeb3f99bff70f79c38bf36ab", - "version": "1.2.0" - } - }, { "package": "Get", "repositoryURL": "https://github.com/kean/Get", diff --git a/Jel/Models/BlurHashDecode.swift b/Jel/Models/BlurHashDecode.swift new file mode 100644 index 0000000..93c4896 --- /dev/null +++ b/Jel/Models/BlurHashDecode.swift @@ -0,0 +1,153 @@ +// +// BlurHashDecode.swift +// Jel +// +// Created by zerocool on 12/22/23. +// + +import UIKit + +extension UIImage { + public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { + guard blurHash.count >= 6 else { return nil } + + let sizeFlag = String(blurHash[0]).decode83() + let numY = (sizeFlag / 9) + 1 + let numX = (sizeFlag % 9) + 1 + + let quantisedMaximumValue = String(blurHash[1]).decode83() + let maximumValue = Float(quantisedMaximumValue + 1) / 166 + + guard blurHash.count == 4 + 2 * numX * numY else { return nil } + + let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in + if i == 0 { + let value = String(blurHash[2 ..< 6]).decode83() + return decodeDC(value) + } else { + let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() + return decodeAC(value, maximumValue: maximumValue * punch) + } + } + + let width = Int(size.width) + let height = Int(size.height) + let bytesPerRow = width * 3 + guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil } + CFDataSetLength(data, bytesPerRow * height) + guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } + + for y in 0 ..< height { + for x in 0 ..< width { + var r: Float = 0 + var g: Float = 0 + var b: Float = 0 + + for j in 0 ..< numY { + for i in 0 ..< numX { + let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) + let colour = colours[i + j * numX] + r += colour.0 * basis + g += colour.1 * basis + b += colour.2 * basis + } + } + + let intR = UInt8(linearTosRGB(r)) + let intG = UInt8(linearTosRGB(g)) + let intB = UInt8(linearTosRGB(b)) + + pixels[3 * x + 0 + y * bytesPerRow] = intR + pixels[3 * x + 1 + y * bytesPerRow] = intG + pixels[3 * x + 2 + y * bytesPerRow] = intB + } + } + + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) + + guard let provider = CGDataProvider(data: data) else { return nil } + guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil } + + self.init(cgImage: cgImage) + } +} + +private func decodeDC(_ value: Int) -> (Float, Float, Float) { + let intR = value >> 16 + let intG = (value >> 8) & 255 + let intB = value & 255 + return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) +} + +private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { + let quantR = value / (19 * 19) + let quantG = (value / 19) % 19 + let quantB = value % 19 + + let rgb = ( + signPow((Float(quantR) - 9) / 9, 2) * maximumValue, + signPow((Float(quantG) - 9) / 9, 2) * maximumValue, + signPow((Float(quantB) - 9) / 9, 2) * maximumValue + ) + + return rgb +} + +private func signPow(_ value: Float, _ exp: Float) -> Float { + return copysign(pow(abs(value), exp), value) +} + +private func linearTosRGB(_ value: Float) -> Int { + let v = max(0, min(1, value)) + if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } + else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } +} + +private func sRGBToLinear(_ value: Type) -> Float { + let v = Float(Int64(value)) / 255 + if v <= 0.04045 { return v / 12.92 } + else { return pow((v + 0.055) / 1.055, 2.4) } +} + +private let encodeCharacters: [String] = { + return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) } +}() + +private let decodeCharacters: [String: Int] = { + var dict: [String: Int] = [:] + for (index, character) in encodeCharacters.enumerated() { + dict[character] = index + } + return dict +}() + +extension String { + func decode83() -> Int { + var value: Int = 0 + for character in self { + if let digit = decodeCharacters[String(character)] { + value = value * 83 + digit + } + } + return value + } +} + +private extension String { + subscript (offset: Int) -> Character { + return self[index(startIndex, offsetBy: offset)] + } + + subscript (bounds: CountableClosedRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start...end] + } + + subscript (bounds: CountableRange) -> Substring { + let start = index(startIndex, offsetBy: bounds.lowerBound) + let end = index(startIndex, offsetBy: bounds.upperBound) + return self[start..