17 changed files with 718 additions and 90 deletions
-
8kplayer.xcodeproj/project.pbxproj
-
41kplayer/core/DatabaseManager.swift
-
4kplayer/core/KSettings.swift
-
9kplayer/core/MediaItem.swift
-
72kplayer/core/NetworkManager.swift
-
9kplayer/detail/DetailViewController+Show.swift
-
25kplayer/detail/DetailViewController.swift
-
7kplayer/detail/EditItemView.swift
-
64kplayer/photo/SPhotoAlbumView.swift
-
124kplayer/photo/SPhotoModel.swift
-
20kplayer/photo/SPhotoScrubber.swift
-
80kplayer/photo/SPhotoView.swift
-
104kplayer/server/kplayer.js
-
11kplayer/util/AsyncImage.swift
-
113kplayer/util/ImageCache.swift
-
99kplayer/util/ImageLoader.swift
-
18kplayer/util/UIImageExtension.swift
@ -0,0 +1,113 @@ |
|||||
|
import Foundation |
||||
|
import UIKit.UIImage |
||||
|
import Combine |
||||
|
|
||||
|
// https://medium.com/@mshcheglov/reusable-image-cache-in-swift-9b90eb338e8d |
||||
|
// https://github.com/sgl0v/OnSwiftWings/blob/master/ImageCache.playground/Sources/MovieTableViewCell.swift |
||||
|
|
||||
|
// Declares in-memory image cache |
||||
|
public protocol ImageCacheType: class { |
||||
|
// Returns the image associated with a given url |
||||
|
func image(for url: URL) -> UIImage? |
||||
|
// Inserts the image of the specified url in the cache |
||||
|
func insertImage(_ image: UIImage?, for url: URL) |
||||
|
// Removes the image of the specified url in the cache |
||||
|
func removeImage(for url: URL) |
||||
|
// Removes all images from the cache |
||||
|
func removeAllImages() |
||||
|
// Accesses the value associated with the given key for reading and writing |
||||
|
subscript(_ url: URL) -> UIImage? { get set } |
||||
|
} |
||||
|
|
||||
|
public final class ImageCache: ImageCacheType { |
||||
|
|
||||
|
// 1st level cache, that contains encoded images |
||||
|
private lazy var imageCache: NSCache<AnyObject, AnyObject> = { |
||||
|
let cache = NSCache<AnyObject, AnyObject>() |
||||
|
cache.countLimit = config.countLimit |
||||
|
return cache |
||||
|
}() |
||||
|
// 2nd level cache, that contains decoded images |
||||
|
private lazy var decodedImageCache: NSCache<AnyObject, AnyObject> = { |
||||
|
let cache = NSCache<AnyObject, AnyObject>() |
||||
|
cache.totalCostLimit = config.memoryLimit |
||||
|
return cache |
||||
|
}() |
||||
|
private let lock = NSLock() |
||||
|
private let config: Config |
||||
|
|
||||
|
public struct Config { |
||||
|
public let countLimit: Int |
||||
|
public let memoryLimit: Int |
||||
|
|
||||
|
public static let defaultConfig = Config(countLimit: 100, memoryLimit: 1024 * 1024 * 100) // 100 MB |
||||
|
} |
||||
|
|
||||
|
public init(config: Config = Config.defaultConfig) { |
||||
|
self.config = config |
||||
|
} |
||||
|
|
||||
|
public func image(for url: URL) -> UIImage? { |
||||
|
lock.lock(); defer { lock.unlock() } |
||||
|
// the best case scenario -> there is a decoded image in memory |
||||
|
if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage { |
||||
|
return decodedImage |
||||
|
} |
||||
|
// search for image data |
||||
|
if let image = imageCache.object(forKey: url as AnyObject) as? UIImage { |
||||
|
let decodedImage = image.decodedImage() |
||||
|
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize) |
||||
|
return decodedImage |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
public func insertImage(_ image: UIImage?, for url: URL) { |
||||
|
guard let image = image else { return removeImage(for: url) } |
||||
|
let decompressedImage = image.decodedImage() |
||||
|
|
||||
|
lock.lock(); defer { lock.unlock() } |
||||
|
imageCache.setObject(decompressedImage, forKey: url as AnyObject, cost: 1) |
||||
|
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decompressedImage.diskSize) |
||||
|
} |
||||
|
|
||||
|
public func removeImage(for url: URL) { |
||||
|
lock.lock(); defer { lock.unlock() } |
||||
|
imageCache.removeObject(forKey: url as AnyObject) |
||||
|
decodedImageCache.removeObject(forKey: url as AnyObject) |
||||
|
} |
||||
|
|
||||
|
public func removeAllImages() { |
||||
|
lock.lock(); defer { lock.unlock() } |
||||
|
imageCache.removeAllObjects() |
||||
|
decodedImageCache.removeAllObjects() |
||||
|
} |
||||
|
|
||||
|
public subscript(_ key: URL) -> UIImage? { |
||||
|
get { |
||||
|
return image(for: key) |
||||
|
} |
||||
|
set { |
||||
|
return insertImage(newValue, for: key) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fileprivate extension UIImage { |
||||
|
|
||||
|
func decodedImage() -> UIImage { |
||||
|
guard let cgImage = cgImage else { return self } |
||||
|
let size = CGSize(width: cgImage.width, height: cgImage.height) |
||||
|
let colorSpace = CGColorSpaceCreateDeviceRGB() |
||||
|
let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) |
||||
|
context?.draw(cgImage, in: CGRect(origin: .zero, size: size)) |
||||
|
guard let decodedImage = context?.makeImage() else { return self } |
||||
|
return UIImage(cgImage: decodedImage) |
||||
|
} |
||||
|
|
||||
|
// Rough estimation of how much memory image uses in bytes |
||||
|
var diskSize: Int { |
||||
|
guard let cgImage = cgImage else { return 0 } |
||||
|
return cgImage.bytesPerRow * cgImage.height |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
import Foundation |
||||
|
import UIKit.UIImage |
||||
|
import Combine |
||||
|
|
||||
|
public final class ImageLoader { |
||||
|
public static let shared = ImageLoader() |
||||
|
|
||||
|
public let cache: ImageCacheType |
||||
|
public lazy var backgroundQueue: OperationQueue = { |
||||
|
let queue = OperationQueue() |
||||
|
queue.maxConcurrentOperationCount = 5 |
||||
|
queue.qualityOfService = .userInteractive |
||||
|
return queue |
||||
|
}() |
||||
|
|
||||
|
private lazy var backgroundQueue2: OperationQueue = { |
||||
|
let queue = OperationQueue() |
||||
|
queue.maxConcurrentOperationCount = 5 |
||||
|
queue.qualityOfService = .userInteractive |
||||
|
return queue |
||||
|
}() |
||||
|
|
||||
|
public init(cache: ImageCacheType = ImageCache()) { |
||||
|
self.cache = cache |
||||
|
} |
||||
|
|
||||
|
public func loadImage(from url: URL) -> AnyPublisher<UIImage?, Never> { |
||||
|
loadImage(from: url, queue: backgroundQueue) |
||||
|
} |
||||
|
|
||||
|
public func loadImageBackground(from url: URL) -> AnyPublisher<UIImage?, Never> { |
||||
|
loadImage(from: url, queue: backgroundQueue) |
||||
|
} |
||||
|
|
||||
|
public func loadImage(from url: URL, queue: OperationQueue) -> AnyPublisher<UIImage?, Never> { |
||||
|
let bigUrl = URL(string: url.absoluteString.replacingOccurrences(of: "/ren/thumbs/", with: "/ren/"))! |
||||
|
if let image = cache[bigUrl] { |
||||
|
return Just(image).eraseToAnyPublisher() |
||||
|
} |
||||
|
return URLSession.shared.dataTaskPublisher(for: url) |
||||
|
.map { (data, response) -> UIImage? in |
||||
|
if let i = UIImage(data: data) { |
||||
|
return i.scaleDown(3000) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
.catch { error in return Just(nil) } |
||||
|
.handleEvents(receiveOutput: {[unowned self] image in |
||||
|
guard let image = image else { return } |
||||
|
self.cache[bigUrl] = image |
||||
|
|
||||
|
print("Thumb Image loaded \(url):") |
||||
|
}) |
||||
|
// .print("Image loading \(url):") |
||||
|
.subscribe(on: queue) |
||||
|
.receive(on: RunLoop.main) |
||||
|
.eraseToAnyPublisher() |
||||
|
} |
||||
|
|
||||
|
public func loadFullImage(from url: URL) -> AnyPublisher<UIImage?, Never> { |
||||
|
print("Full Image loading \(url):") |
||||
|
|
||||
|
backgroundQueue2.cancelAllOperations() |
||||
|
|
||||
|
return URLSession.shared.dataTaskPublisher(for: url) |
||||
|
.map { (data, response) -> UIImage? in |
||||
|
if let r = response as? HTTPURLResponse { |
||||
|
if r.statusCode >= 400 { |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
if let i = UIImage(data: data) { |
||||
|
|
||||
|
if i.size.height > 3000 { |
||||
|
let targetSize = CGSize(width: 3000, height: 3000) |
||||
|
|
||||
|
// Compute the scaling ratio for the width and height separately |
||||
|
let widthScaleRatio = targetSize.width / i.size.width |
||||
|
let heightScaleRatio = targetSize.height / i.size.height |
||||
|
|
||||
|
// To keep the aspect ratio, scale by the smaller scaling ratio |
||||
|
let scaleFactor = min(widthScaleRatio, heightScaleRatio) |
||||
|
self.cache[url] = i.scaleToSize(i.size.width * scaleFactor, height: i.size.height * scaleFactor) |
||||
|
|
||||
|
} else { |
||||
|
self.cache[url] = i |
||||
|
} |
||||
|
|
||||
|
return i |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
.catch { error in return Just(nil) } |
||||
|
// .print("Full Image loading \(url):") |
||||
|
.subscribe(on: backgroundQueue2) |
||||
|
.receive(on: RunLoop.main) |
||||
|
.eraseToAnyPublisher() |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue