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