Image caching
This commit is contained in:
parent
516469427e
commit
ef86972724
@ -91,6 +91,12 @@ struct ContentView: View {
|
|||||||
)
|
)
|
||||||
.frame(minWidth: 450)
|
.frame(minWidth: 450)
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
|
if library.isRestoringPersistedSources {
|
||||||
|
LaunchRestoreOverlayView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(library.isRestoringPersistedSources)
|
||||||
.onChange(of: displayedItems.map(\.id)) { _, filteredIDs in
|
.onChange(of: displayedItems.map(\.id)) { _, filteredIDs in
|
||||||
guard let selectedItemID, !filteredIDs.contains(selectedItemID) else {
|
guard let selectedItemID, !filteredIDs.contains(selectedItemID) else {
|
||||||
return
|
return
|
||||||
@ -1315,6 +1321,33 @@ private struct SharingPickerButton: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct LaunchRestoreOverlayView: View {
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Rectangle()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.large)
|
||||||
|
|
||||||
|
Text("Restoring Saved Library")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
|
||||||
|
Text("Loading saved sources, metadata, and cached artwork.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
.padding(.vertical, 28)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 18)
|
||||||
|
.fill(.background.opacity(0.92))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct PackReferenceIconView: View {
|
private struct PackReferenceIconView: View {
|
||||||
let iconURL: URL?
|
let iconURL: URL?
|
||||||
|
|
||||||
|
|||||||
97
World Manager for Minecraft/Services/ImageCacheStore.swift
Normal file
97
World Manager for Minecraft/Services/ImageCacheStore.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// ImageCacheStore.swift
|
||||||
|
// World Manager for Minecraft
|
||||||
|
//
|
||||||
|
// Created by OpenAI on 2026-05-26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
actor ImageCacheStore {
|
||||||
|
static let shared = ImageCacheStore()
|
||||||
|
|
||||||
|
private let fileManager: FileManager
|
||||||
|
private let cacheDirectoryURL: URL
|
||||||
|
private let cacheDirectoryPath: String
|
||||||
|
|
||||||
|
init(fileManager: FileManager = .default) {
|
||||||
|
self.fileManager = fileManager
|
||||||
|
|
||||||
|
let cachesURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||||
|
?? fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Caches", isDirectory: true)
|
||||||
|
let cacheDirectoryURL = cachesURL
|
||||||
|
.appendingPathComponent("World Manager for Minecraft", isDirectory: true)
|
||||||
|
.appendingPathComponent("ImageCache", isDirectory: true)
|
||||||
|
|
||||||
|
self.cacheDirectoryURL = cacheDirectoryURL
|
||||||
|
self.cacheDirectoryPath = cacheDirectoryURL.standardizedFileURL.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachedImageURL(for sourceImageURL: URL?) -> URL? {
|
||||||
|
guard let sourceImageURL else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedSourceURL = sourceImageURL.standardizedFileURL
|
||||||
|
if isCachedImageURL(normalizedSourceURL) {
|
||||||
|
return fileManager.fileExists(atPath: normalizedSourceURL.path) ? normalizedSourceURL : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard fileManager.fileExists(atPath: normalizedSourceURL.path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard
|
||||||
|
let resourceValues = try? normalizedSourceURL.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey]),
|
||||||
|
let fileSize = resourceValues.fileSize
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let modifiedStamp = resourceValues.contentModificationDate?.timeIntervalSince1970 ?? 0
|
||||||
|
let sourceKey = digest(for: normalizedSourceURL.path)
|
||||||
|
let versionKey = digest(for: "\(normalizedSourceURL.path)|\(modifiedStamp)|\(fileSize)")
|
||||||
|
let pathExtension = normalizedSourceURL.pathExtension.isEmpty ? "img" : normalizedSourceURL.pathExtension
|
||||||
|
let cachedURL = cacheDirectoryURL
|
||||||
|
.appendingPathComponent("\(sourceKey)-\(versionKey)", isDirectory: false)
|
||||||
|
.appendingPathExtension(pathExtension)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try fileManager.createDirectory(at: cacheDirectoryURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
if fileManager.fileExists(atPath: cachedURL.path) {
|
||||||
|
return cachedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeStaleVariants(forSourceKey: sourceKey, keeping: cachedURL)
|
||||||
|
try fileManager.copyItem(at: normalizedSourceURL, to: cachedURL)
|
||||||
|
return cachedURL
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCachedImageURL(_ url: URL) -> Bool {
|
||||||
|
url.standardizedFileURL.path.hasPrefix(cacheDirectoryPath + "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func purgeStaleVariants(forSourceKey sourceKey: String, keeping cachedURL: URL) {
|
||||||
|
guard let cachedFiles = try? fileManager.contentsOfDirectory(
|
||||||
|
at: cacheDirectoryURL,
|
||||||
|
includingPropertiesForKeys: nil,
|
||||||
|
options: [.skipsHiddenFiles]
|
||||||
|
) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for url in cachedFiles where url.lastPathComponent.hasPrefix(sourceKey + "-") && url != cachedURL {
|
||||||
|
try? fileManager.removeItem(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func digest(for value: String) -> String {
|
||||||
|
let bytes = SHA256.hash(data: Data(value.utf8))
|
||||||
|
return bytes.map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,6 +34,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
subtitle: nil,
|
subtitle: nil,
|
||||||
revealURL: nil
|
revealURL: nil
|
||||||
)
|
)
|
||||||
|
@Published private(set) var isRestoringPersistedSources = true
|
||||||
|
|
||||||
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
private var scanTasks: [URL: Task<Void, Never>] = [:]
|
||||||
private var footerResetTask: Task<Void, Never>?
|
private var footerResetTask: Task<Void, Never>?
|
||||||
@ -168,6 +169,10 @@ final class SourceLibrary: ObservableObject {
|
|||||||
let sizeQueue = EnrichmentWorkQueue()
|
let sizeQueue = EnrichmentWorkQueue()
|
||||||
workerTasks = (0..<Self.enrichmentWorkerCount).map { _ in
|
workerTasks = (0..<Self.enrichmentWorkerCount).map { _ in
|
||||||
Task.detached(priority: .utility) { [weak self] in
|
Task.detached(priority: .utility) { [weak self] in
|
||||||
|
guard let library = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
while let item = await enrichmentQueue.next() {
|
while let item = await enrichmentQueue.next() {
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
@ -176,12 +181,8 @@ final class SourceLibrary: ObservableObject {
|
|||||||
let enrichedItem = await WorldScanner.enrich(item: item)
|
let enrichedItem = await WorldScanner.enrich(item: item)
|
||||||
if let snapshot = await index.applyEnrichedItem(enrichedItem) {
|
if let snapshot = await index.applyEnrichedItem(enrichedItem) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard let self else {
|
library.applySnapshot(snapshot, to: sourceID)
|
||||||
return
|
library.refreshSidebarFooterState()
|
||||||
}
|
|
||||||
|
|
||||||
self.applySnapshot(snapshot, to: sourceID)
|
|
||||||
self.refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await sizeQueue.enqueue(enrichedItem)
|
await sizeQueue.enqueue(enrichedItem)
|
||||||
@ -190,6 +191,10 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
sizeWorkerTasks = (0..<Self.sizeWorkerCount).map { _ in
|
sizeWorkerTasks = (0..<Self.sizeWorkerCount).map { _ in
|
||||||
Task.detached(priority: .utility) { [weak self] in
|
Task.detached(priority: .utility) { [weak self] in
|
||||||
|
guard let library = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
while let item = await sizeQueue.next() {
|
while let item = await sizeQueue.next() {
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
@ -198,12 +203,8 @@ final class SourceLibrary: ObservableObject {
|
|||||||
let sizedItem = WorldScanner.loadSize(for: item)
|
let sizedItem = WorldScanner.loadSize(for: item)
|
||||||
if let snapshot = await index.applySizedItem(sizedItem) {
|
if let snapshot = await index.applySizedItem(sizedItem) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard let self else {
|
library.applySnapshot(snapshot, to: sourceID)
|
||||||
return
|
library.refreshSidebarFooterState()
|
||||||
}
|
|
||||||
|
|
||||||
self.applySnapshot(snapshot, to: sourceID)
|
|
||||||
self.refreshSidebarFooterState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -663,6 +664,11 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func restorePersistedSources() async {
|
private func restorePersistedSources() async {
|
||||||
|
defer {
|
||||||
|
isRestoringPersistedSources = false
|
||||||
|
refreshSidebarFooterState()
|
||||||
|
}
|
||||||
|
|
||||||
let records: [PersistedSourceRecord]
|
let records: [PersistedSourceRecord]
|
||||||
do {
|
do {
|
||||||
records = try await persistenceStore.loadSources()
|
records = try await persistenceStore.loadSources()
|
||||||
@ -673,9 +679,9 @@ final class SourceLibrary: ObservableObject {
|
|||||||
for record in records {
|
for record in records {
|
||||||
var source = MinecraftSource(folderURL: record.folderURL)
|
var source = MinecraftSource(folderURL: record.folderURL)
|
||||||
source.displayName = record.displayName
|
source.displayName = record.displayName
|
||||||
source.rawItems = record.rawItems
|
source.rawItems = await restoreCachedImages(in: record.rawItems)
|
||||||
source.indexedItemCount = record.rawItems.count
|
source.indexedItemCount = record.rawItems.count
|
||||||
source.indexedDetailCount = record.rawItems.filter(\.metadataLoaded).count
|
source.indexedDetailCount = source.rawItems.filter(\.metadataLoaded).count
|
||||||
source.lastScanDate = record.lastScanDate
|
source.lastScanDate = record.lastScanDate
|
||||||
source.snapshot = record.snapshot
|
source.snapshot = record.snapshot
|
||||||
|
|
||||||
@ -691,7 +697,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
||||||
refreshSidebarFooterState()
|
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
if sourceNeedsRescan(record) {
|
if sourceNeedsRescan(record) {
|
||||||
@ -700,6 +705,40 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func restoreCachedImages(in items: [MinecraftContentItem]) async -> [MinecraftContentItem] {
|
||||||
|
var restoredItems: [MinecraftContentItem] = []
|
||||||
|
restoredItems.reserveCapacity(items.count)
|
||||||
|
|
||||||
|
for var item in items {
|
||||||
|
item.iconURL = await ImageCacheStore.shared.cachedImageURL(for: item.iconURL)
|
||||||
|
item.packReferences = await restoreCachedImages(in: item.packReferences)
|
||||||
|
restoredItems.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoredItems
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreCachedImages(in references: [ContentPackReference]) async -> [ContentPackReference] {
|
||||||
|
var restoredReferences: [ContentPackReference] = []
|
||||||
|
restoredReferences.reserveCapacity(references.count)
|
||||||
|
|
||||||
|
for reference in references {
|
||||||
|
let cachedIconURL = await ImageCacheStore.shared.cachedImageURL(for: reference.iconURL)
|
||||||
|
restoredReferences.append(
|
||||||
|
ContentPackReference(
|
||||||
|
name: reference.name,
|
||||||
|
type: reference.type,
|
||||||
|
iconURL: cachedIconURL,
|
||||||
|
uuid: reference.uuid,
|
||||||
|
version: reference.version,
|
||||||
|
source: reference.source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoredReferences
|
||||||
|
}
|
||||||
|
|
||||||
private func sourceNeedsRescan(_ record: PersistedSourceRecord) -> Bool {
|
private func sourceNeedsRescan(_ record: PersistedSourceRecord) -> Bool {
|
||||||
guard let snapshot = record.snapshot else {
|
guard let snapshot = record.snapshot else {
|
||||||
return true
|
return true
|
||||||
@ -833,6 +872,17 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func refreshSidebarFooterState() {
|
private func refreshSidebarFooterState() {
|
||||||
|
if isRestoringPersistedSources {
|
||||||
|
cancelFooterReset()
|
||||||
|
sidebarFooterState = SidebarFooterState(
|
||||||
|
style: .inProgress,
|
||||||
|
title: "Restoring library...",
|
||||||
|
subtitle: "Loading saved sources and cached metadata",
|
||||||
|
revealURL: nil
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let scanningSources = sources.filter(\.isScanning)
|
let scanningSources = sources.filter(\.isScanning)
|
||||||
if let source = scanningSources.first {
|
if let source = scanningSources.first {
|
||||||
cancelFooterReset()
|
cancelFooterReset()
|
||||||
|
|||||||
@ -16,7 +16,7 @@ struct PersistedSourceRecord: Sendable {
|
|||||||
let lastScanDate: Date?
|
let lastScanDate: Date?
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PersistedItemSnapshotPayload: Codable {
|
private struct PersistedItemSnapshotPayload: Codable, Sendable {
|
||||||
let idPath: String
|
let idPath: String
|
||||||
let relativePath: String
|
let relativePath: String
|
||||||
let modifiedDate: Date?
|
let modifiedDate: Date?
|
||||||
@ -24,7 +24,7 @@ private struct PersistedItemSnapshotPayload: Codable {
|
|||||||
let packUUID: String?
|
let packUUID: String?
|
||||||
let packVersion: String?
|
let packVersion: String?
|
||||||
|
|
||||||
init(_ snapshot: ItemSnapshot) {
|
nonisolated init(_ snapshot: ItemSnapshot) {
|
||||||
self.idPath = snapshot.id.path
|
self.idPath = snapshot.id.path
|
||||||
self.relativePath = snapshot.relativePath
|
self.relativePath = snapshot.relativePath
|
||||||
self.modifiedDate = snapshot.modifiedDate
|
self.modifiedDate = snapshot.modifiedDate
|
||||||
@ -33,7 +33,7 @@ private struct PersistedItemSnapshotPayload: Codable {
|
|||||||
self.packVersion = snapshot.packVersion
|
self.packVersion = snapshot.packVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemSnapshot: ItemSnapshot {
|
nonisolated var itemSnapshot: ItemSnapshot {
|
||||||
ItemSnapshot(
|
ItemSnapshot(
|
||||||
id: URL(fileURLWithPath: idPath),
|
id: URL(fileURLWithPath: idPath),
|
||||||
relativePath: relativePath,
|
relativePath: relativePath,
|
||||||
@ -43,22 +43,51 @@ private struct PersistedItemSnapshotPayload: Codable {
|
|||||||
packVersion: packVersion
|
packVersion: packVersion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.idPath = try container.decode(String.self, forKey: .idPath)
|
||||||
|
self.relativePath = try container.decode(String.self, forKey: .relativePath)
|
||||||
|
self.modifiedDate = try container.decodeIfPresent(Date.self, forKey: .modifiedDate)
|
||||||
|
self.sizeBytes = try container.decodeIfPresent(Int64.self, forKey: .sizeBytes)
|
||||||
|
self.packUUID = try container.decodeIfPresent(String.self, forKey: .packUUID)
|
||||||
|
self.packVersion = try container.decodeIfPresent(String.self, forKey: .packVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(idPath, forKey: .idPath)
|
||||||
|
try container.encode(relativePath, forKey: .relativePath)
|
||||||
|
try container.encodeIfPresent(modifiedDate, forKey: .modifiedDate)
|
||||||
|
try container.encodeIfPresent(sizeBytes, forKey: .sizeBytes)
|
||||||
|
try container.encodeIfPresent(packUUID, forKey: .packUUID)
|
||||||
|
try container.encodeIfPresent(packVersion, forKey: .packVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case idPath
|
||||||
|
case relativePath
|
||||||
|
case modifiedDate
|
||||||
|
case sizeBytes
|
||||||
|
case packUUID
|
||||||
|
case packVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PersistedCollectionSnapshotPayload: Codable {
|
private struct PersistedCollectionSnapshotPayload: Codable, Sendable {
|
||||||
let folderName: String
|
let folderName: String
|
||||||
let modifiedDate: Date?
|
let modifiedDate: Date?
|
||||||
let childDirectoryCount: Int
|
let childDirectoryCount: Int
|
||||||
let fingerprint: String
|
let fingerprint: String
|
||||||
|
|
||||||
init(_ snapshot: CollectionSnapshot) {
|
nonisolated init(_ snapshot: CollectionSnapshot) {
|
||||||
self.folderName = snapshot.folderName
|
self.folderName = snapshot.folderName
|
||||||
self.modifiedDate = snapshot.modifiedDate
|
self.modifiedDate = snapshot.modifiedDate
|
||||||
self.childDirectoryCount = snapshot.childDirectoryCount
|
self.childDirectoryCount = snapshot.childDirectoryCount
|
||||||
self.fingerprint = snapshot.fingerprint
|
self.fingerprint = snapshot.fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
var collectionSnapshot: CollectionSnapshot {
|
nonisolated var collectionSnapshot: CollectionSnapshot {
|
||||||
CollectionSnapshot(
|
CollectionSnapshot(
|
||||||
folderName: folderName,
|
folderName: folderName,
|
||||||
modifiedDate: modifiedDate,
|
modifiedDate: modifiedDate,
|
||||||
@ -66,22 +95,45 @@ private struct PersistedCollectionSnapshotPayload: Codable {
|
|||||||
fingerprint: fingerprint
|
fingerprint: fingerprint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.folderName = try container.decode(String.self, forKey: .folderName)
|
||||||
|
self.modifiedDate = try container.decodeIfPresent(Date.self, forKey: .modifiedDate)
|
||||||
|
self.childDirectoryCount = try container.decode(Int.self, forKey: .childDirectoryCount)
|
||||||
|
self.fingerprint = try container.decode(String.self, forKey: .fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(folderName, forKey: .folderName)
|
||||||
|
try container.encodeIfPresent(modifiedDate, forKey: .modifiedDate)
|
||||||
|
try container.encode(childDirectoryCount, forKey: .childDirectoryCount)
|
||||||
|
try container.encode(fingerprint, forKey: .fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case folderName
|
||||||
|
case modifiedDate
|
||||||
|
case childDirectoryCount
|
||||||
|
case fingerprint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PersistedSourceSnapshotPayload: Codable {
|
private struct PersistedSourceSnapshotPayload: Codable, Sendable {
|
||||||
let sourcePath: String
|
let sourcePath: String
|
||||||
let rootModifiedDate: Date?
|
let rootModifiedDate: Date?
|
||||||
let collectionSnapshots: [PersistedCollectionSnapshotPayload]
|
let collectionSnapshots: [PersistedCollectionSnapshotPayload]
|
||||||
let itemSnapshots: [PersistedItemSnapshotPayload]
|
let itemSnapshots: [PersistedItemSnapshotPayload]
|
||||||
|
|
||||||
init(_ snapshot: SourceSnapshot) {
|
nonisolated init(_ snapshot: SourceSnapshot) {
|
||||||
self.sourcePath = snapshot.sourceID.path
|
self.sourcePath = snapshot.sourceID.path
|
||||||
self.rootModifiedDate = snapshot.rootModifiedDate
|
self.rootModifiedDate = snapshot.rootModifiedDate
|
||||||
self.collectionSnapshots = snapshot.collectionSnapshots.map(PersistedCollectionSnapshotPayload.init)
|
self.collectionSnapshots = snapshot.collectionSnapshots.map(PersistedCollectionSnapshotPayload.init)
|
||||||
self.itemSnapshots = snapshot.itemSnapshots.map(PersistedItemSnapshotPayload.init)
|
self.itemSnapshots = snapshot.itemSnapshots.map(PersistedItemSnapshotPayload.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceSnapshot: SourceSnapshot {
|
nonisolated var sourceSnapshot: SourceSnapshot {
|
||||||
SourceSnapshot(
|
SourceSnapshot(
|
||||||
sourceID: URL(fileURLWithPath: sourcePath),
|
sourceID: URL(fileURLWithPath: sourcePath),
|
||||||
rootModifiedDate: rootModifiedDate,
|
rootModifiedDate: rootModifiedDate,
|
||||||
@ -89,6 +141,29 @@ private struct PersistedSourceSnapshotPayload: Codable {
|
|||||||
itemSnapshots: itemSnapshots.map(\.itemSnapshot)
|
itemSnapshots: itemSnapshots.map(\.itemSnapshot)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.sourcePath = try container.decode(String.self, forKey: .sourcePath)
|
||||||
|
self.rootModifiedDate = try container.decodeIfPresent(Date.self, forKey: .rootModifiedDate)
|
||||||
|
self.collectionSnapshots = try container.decode([PersistedCollectionSnapshotPayload].self, forKey: .collectionSnapshots)
|
||||||
|
self.itemSnapshots = try container.decode([PersistedItemSnapshotPayload].self, forKey: .itemSnapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(sourcePath, forKey: .sourcePath)
|
||||||
|
try container.encodeIfPresent(rootModifiedDate, forKey: .rootModifiedDate)
|
||||||
|
try container.encode(collectionSnapshots, forKey: .collectionSnapshots)
|
||||||
|
try container.encode(itemSnapshots, forKey: .itemSnapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case sourcePath
|
||||||
|
case rootModifiedDate
|
||||||
|
case collectionSnapshots
|
||||||
|
case itemSnapshots
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actor SourcePersistenceStore {
|
actor SourcePersistenceStore {
|
||||||
|
|||||||
@ -91,7 +91,8 @@ enum WorldScanner {
|
|||||||
var enrichedItem = item
|
var enrichedItem = item
|
||||||
|
|
||||||
enrichedItem.displayName = displayName(for: item, fileManager: fileManager)
|
enrichedItem.displayName = displayName(for: item, fileManager: fileManager)
|
||||||
enrichedItem.iconURL = iconURL(for: item, fileManager: fileManager)
|
let sourceIconURL = iconURL(for: item, fileManager: fileManager)
|
||||||
|
enrichedItem.iconURL = await ImageCacheStore.shared.cachedImageURL(for: sourceIconURL)
|
||||||
enrichedItem.lastPlayedDate = lastPlayedDate(for: item, fileManager: fileManager)
|
enrichedItem.lastPlayedDate = lastPlayedDate(for: item, fileManager: fileManager)
|
||||||
enrichedItem.modifiedDate = modifiedDate(for: item.folderURL)
|
enrichedItem.modifiedDate = modifiedDate(for: item.folderURL)
|
||||||
if let manifestMetadata = manifestMetadata(in: item.folderURL, fileManager: fileManager) {
|
if let manifestMetadata = manifestMetadata(in: item.folderURL, fileManager: fileManager) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user