Implement library view

This commit is contained in:
Shav Kinderlehrer 2023-12-22 19:29:07 -05:00
parent 5b78933de7
commit 568b4d6126
6 changed files with 170 additions and 32 deletions

View File

@ -11,12 +11,12 @@
3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */; }; 3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DA2B27F5D300F5C29A /* Model.xcdatamodeld */; };
3D1015DE2B27F79900F5C29A /* DatamodelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */; }; 3D1015DE2B27F79900F5C29A /* DatamodelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015DD2B27F79900F5C29A /* DatamodelController.swift */; };
3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1015E32B28000E00F5C29A /* AuthStateController.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 */; }; 3D16FC3C2B2CDFB500E6D8B3 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D16FC3B2B2CDFB500E6D8B3 /* LibraryView.swift */; };
3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */; }; 3D41D1F52B2C962500E58234 /* AppearancePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */; };
3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */; }; 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */; };
3D7709392B29139700199889 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7709382B29139700199889 /* Pulse */; }; 3D7709392B29139700199889 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7709382B29139700199889 /* Pulse */; };
3D77093B2B29139700199889 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3D77093A2B29139700199889 /* PulseUI */; }; 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 */; }; 3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CA2B279A310063DD2A /* JelApp.swift */; };
3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CC2B279A310063DD2A /* ContentView.swift */; }; 3D9063CD2B279A310063DD2A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063CC2B279A310063DD2A /* ContentView.swift */; };
3D9063CF2B279A320063DD2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D9063CE2B279A320063DD2A /* Assets.xcassets */; }; 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 = "<group>"; }; 3D16FC3B2B2CDFB500E6D8B3 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
3D41D1F42B2C962500E58234 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = "<group>"; }; 3D41D1F42B2C962500E58234 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = "<group>"; };
3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryIconView.swift; sourceTree = "<group>"; }; 3D41D1F92B2CAE0000E58234 /* LibraryIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryIconView.swift; sourceTree = "<group>"; };
3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
3D9063C72B279A310063DD2A /* Jel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 3D9063CA2B279A310063DD2A /* JelApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelApp.swift; sourceTree = "<group>"; };
3D9063CC2B279A310063DD2A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 3D9063CC2B279A310063DD2A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -101,7 +102,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3D16FC3A2B2CC22A00E6D8B3 /* BlurHashKit in Frameworks */,
3D77093B2B29139700199889 /* PulseUI in Frameworks */, 3D77093B2B29139700199889 /* PulseUI in Frameworks */,
3D7709392B29139700199889 /* Pulse in Frameworks */, 3D7709392B29139700199889 /* Pulse in Frameworks */,
3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */, 3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */,
@ -152,6 +152,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */, 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */,
3D8AB2A42B36440D005BD7D0 /* BlurHashDecode.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -284,7 +285,6 @@
3D9064582B27E4C70063DD2A /* JellyfinKit */, 3D9064582B27E4C70063DD2A /* JellyfinKit */,
3D7709382B29139700199889 /* Pulse */, 3D7709382B29139700199889 /* Pulse */,
3D77093A2B29139700199889 /* PulseUI */, 3D77093A2B29139700199889 /* PulseUI */,
3D16FC392B2CC22A00E6D8B3 /* BlurHashKit */,
); );
productName = Jel; productName = Jel;
productReference = 3D9063C72B279A310063DD2A /* Jel.app */; productReference = 3D9063C72B279A310063DD2A /* Jel.app */;
@ -360,7 +360,6 @@
mainGroup = 3D9063BE2B279A310063DD2A; mainGroup = 3D9063BE2B279A310063DD2A;
packageReferences = ( packageReferences = (
3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */, 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */,
3D16FC382B2CC22A00E6D8B3 /* XCRemoteSwiftPackageReference "BlurHashKit" */,
); );
productRefGroup = 3D9063C82B279A310063DD2A /* Products */; productRefGroup = 3D9063C82B279A310063DD2A /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -414,6 +413,7 @@
3D91FDC92B28C62800919017 /* SignInView.swift in Sources */, 3D91FDC92B28C62800919017 /* SignInView.swift in Sources */,
3DDD67932B293BC40026781E /* DashboardView.swift in Sources */, 3DDD67932B293BC40026781E /* DashboardView.swift in Sources */,
3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */, 3D41D1FA2B2CAE0000E58234 /* LibraryIconView.swift in Sources */,
3D8AB2A52B36440D005BD7D0 /* BlurHashDecode.swift in Sources */,
3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */, 3DC6BA2D2B2A422300416B9F /* SettingsController.swift in Sources */,
3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */, 3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */,
3D16FC3C2B2CDFB500E6D8B3 /* LibraryView.swift in Sources */, 3D16FC3C2B2CDFB500E6D8B3 /* LibraryView.swift in Sources */,
@ -785,14 +785,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference 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" */ = { 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Pulse"; repositoryURL = "https://github.com/kean/Pulse";
@ -804,11 +796,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
3D16FC392B2CC22A00E6D8B3 /* BlurHashKit */ = {
isa = XCSwiftPackageProductDependency;
package = 3D16FC382B2CC22A00E6D8B3 /* XCRemoteSwiftPackageReference "BlurHashKit" */;
productName = BlurHashKit;
};
3D7709382B29139700199889 /* Pulse */ = { 3D7709382B29139700199889 /* Pulse */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */; package = 3D7709372B29139700199889 /* XCRemoteSwiftPackageReference "Pulse" */;

View File

@ -1,15 +1,6 @@
{ {
"object": { "object": {
"pins": [ "pins": [
{
"package": "BlurHashKit",
"repositoryURL": "https://github.com/LePips/BlurHashKit",
"state": {
"branch": null,
"revision": "c0bd7423398de68cbeb3f99bff70f79c38bf36ab",
"version": "1.2.0"
}
},
{ {
"package": "Get", "package": "Get",
"repositoryURL": "https://github.com/kean/Get", "repositoryURL": "https://github.com/kean/Get",

View File

@ -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<Type: BinaryInteger>(_ 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<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}

View File

@ -22,6 +22,10 @@ struct LibraryIconView: View {
AsyncImageView(imageId: library.id ?? "", AsyncImageView(imageId: library.id ?? "",
blurhash: library.imageBlurHashes?.primary?[library.imageTags?["Primary"] ?? ""] ?? "", blurhash: library.imageBlurHashes?.primary?[library.imageTags?["Primary"] ?? ""] ?? "",
imageType: "Primary") imageType: "Primary")
.aspectRatio(contentMode: .fill)
.frame(width: 255, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 5))
Text(library.name ?? "Unknown") Text(library.name ?? "Unknown")
.font(.subheadline) .font(.subheadline)
@ -29,6 +33,6 @@ struct LibraryIconView: View {
} }
} }
#Preview { //#Preview {
LibraryIconView(library: BaseItemDto()) // LibraryIconView(library: BaseItemDto())
} //}

View File

@ -16,7 +16,7 @@ struct LibraryView: View {
@State var libraries: [BaseItemDto] = [] @State var libraries: [BaseItemDto] = []
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack { HStack {
ForEach(libraries) {library in ForEach(libraries) {library in
if library.collectionType == "movies" || library.collectionType == "tvshows" { if library.collectionType == "movies" || library.collectionType == "tvshows" {
LibraryIconView(library: library) LibraryIconView(library: library)

View File

@ -6,7 +6,6 @@
// //
import SwiftUI import SwiftUI
import BlurHashKit
import JellyfinKit import JellyfinKit
struct AsyncImageView: View { struct AsyncImageView: View {
@ -22,12 +21,16 @@ struct AsyncImageView: View {
var body: some View { var body: some View {
VStack { VStack {
if loading { if loading {
BlurHashView(blurHash: blurhash) Image(uiImage: uiImage)
.resizable()
} else { } else {
Image(uiImage: uiImage) Image(uiImage: uiImage)
.resizable()
} }
} }
.onAppear { .onAppear {
uiImage = UIImage(blurHash: blurhash, size: CGSize(width: 16, height: 16)) ?? UIImage()
Task { Task {
let request = Paths.getItemImage(itemID: imageId, imageType: imageType) let request = Paths.getItemImage(itemID: imageId, imageType: imageType)
do { do {