mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
113 lines
3.7 KiB
Swift
113 lines
3.7 KiB
Swift
/*
|
|
* Atoll (DynamicIsland)
|
|
* Copyright (C) 2024-2026 Atoll Contributors
|
|
*
|
|
* Originally from boring.notch project
|
|
* Modified and adapted for Atoll (DynamicIsland)
|
|
* See NOTICE for details.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import Foundation
|
|
import AppKit
|
|
import QuickLookThumbnailing
|
|
import UniformTypeIdentifiers
|
|
|
|
actor ThumbnailService {
|
|
static let shared = ThumbnailService()
|
|
|
|
private var cache: [String: NSImage] = [:]
|
|
private var pendingRequests: [String: Task<NSImage?, Never>] = [:]
|
|
private let thumbnailGenerator = QLThumbnailGenerator.shared
|
|
|
|
private init() {}
|
|
|
|
func thumbnail(for url: URL, size: CGSize) async -> NSImage? {
|
|
let cacheKey = "\(url.path)_\(size.width)x\(size.height)"
|
|
|
|
if let cached = cache[cacheKey] {
|
|
return cached
|
|
}
|
|
|
|
if let pending = pendingRequests[cacheKey] {
|
|
return await pending.value
|
|
}
|
|
|
|
let task = Task<NSImage?, Never> {
|
|
let thumbnail = await generateQuickLookThumbnail(for: url, size: size)
|
|
if let thumbnail = thumbnail {
|
|
cache[cacheKey] = thumbnail
|
|
}
|
|
pendingRequests[cacheKey] = nil
|
|
return thumbnail
|
|
}
|
|
|
|
pendingRequests[cacheKey] = task
|
|
return await task.value
|
|
}
|
|
|
|
func clearCache() {
|
|
cache.removeAll()
|
|
}
|
|
|
|
func clearCache(for url: URL) {
|
|
cache = cache.filter { !$0.key.starts(with: url.path) }
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func generateQuickLookThumbnail(for url: URL, size: CGSize) async -> NSImage? {
|
|
let scale = await MainActor.run { NSScreen.main?.backingScaleFactor ?? 2.0 }
|
|
|
|
return await url.accessSecurityScopedResource { scopedURL in
|
|
NSLog("🔐 ThumbnailService: obtaining security scope for \(scopedURL.path)")
|
|
let request = QLThumbnailGenerator.Request(
|
|
fileAt: scopedURL,
|
|
size: size,
|
|
scale: scale,
|
|
representationTypes: .all
|
|
)
|
|
request.iconMode = true
|
|
|
|
return await withCheckedContinuation { (continuation: CheckedContinuation<NSImage?, Never>) in
|
|
thumbnailGenerator.generateBestRepresentation(for: request) { representation, error in
|
|
if let rep = representation {
|
|
NSLog("🔍 ThumbnailService: generated thumbnail for \(scopedURL.path)")
|
|
continuation.resume(returning: rep.nsImage)
|
|
} else {
|
|
if let err = error {
|
|
NSLog("⚠️ ThumbnailService: thumbnail error for \(scopedURL.path): \(err.localizedDescription)")
|
|
}
|
|
continuation.resume(returning: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Extensions
|
|
|
|
extension QLThumbnailRepresentation {
|
|
var nsImage: NSImage {
|
|
return NSImage(cgImage: self.cgImage, size: self.cgImage.size)
|
|
}
|
|
}
|
|
|
|
extension CGImage {
|
|
var size: NSSize {
|
|
return NSSize(width: self.width, height: self.height)
|
|
}
|
|
}
|