Atoll/DynamicIsland/components/Shelf/Services/ThumbnailService.swift
2026-01-29 12:26:04 +05:30

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)
}
}