diff --git a/OpenEmuKitTests/ShaderPresetModelTests.swift b/OpenEmuKitTests/ShaderPresetModelTests.swift index cb5bf77..125f34e 100644 --- a/OpenEmuKitTests/ShaderPresetModelTests.swift +++ b/OpenEmuKitTests/ShaderPresetModelTests.swift @@ -81,13 +81,10 @@ class ShaderPresetModelTests: XCTestCase { func testCanFindPreset() { expect(self.presets.findPreset(byID: "id1")) .toNot(be(nil)) - expect(self.presets.findPreset(byName: "shader 2")) - .toNot(be(nil)) } func testInstancesAreSame() { expect(self.presets.findPreset(byID: "id2")) === presets.findPreset(byID: "id2") - expect(self.presets.findPreset(byName: "shader 1")) === presets.findPreset(byName: "shader 1") } func testFindPresets() { @@ -101,9 +98,6 @@ class ShaderPresetModelTests: XCTestCase { func testExists() { expect(self.presets.exists(byID: "id2")) == true expect(self.presets.exists(byID: "foo")) == false - - expect(self.presets.exists(byName: "shader 1")) == true - expect(self.presets.exists(byName: "foo")) == false } func testRemovePreset() { @@ -121,10 +115,10 @@ class ShaderPresetModelTests: XCTestCase { XCTFail("Expected to find id2") return } - let oldName = a.name a.name = "dummy name" try presets.savePreset(a) - expect(self.presets.findPreset(byName: oldName)).to(beNil()) - expect(self.presets.findPreset(byName: "dummy name")).toNot(beNil()) + guard let b = presets.findPreset(byID: "id2") + else { return XCTFail("Expected to find id2") } + expect(b.name).to(equal("dummy name")) } } diff --git a/OpenEmuKitTests/ShaderPresetStoreTests.swift b/OpenEmuKitTests/ShaderPresetStoreTests.swift index 1d81343..258505f 100644 --- a/OpenEmuKitTests/ShaderPresetStoreTests.swift +++ b/OpenEmuKitTests/ShaderPresetStoreTests.swift @@ -79,34 +79,6 @@ class UserDefaultsPresetStorageTests: XCTestCase { } } - func testFailsForDuplicateName() { - let store = store! - expect { - try store.save(ShaderPresetData(name: "foo", shader: "CRT", parameters: [:], id: "id1")) - try store.save(ShaderPresetData(name: "foo", shader: "MAME", parameters: [:], id: "id2")) - } - .to(throwError(ShaderPresetStorageError.duplicateName)) - } - - func testRenameShaderSucceeds() throws { - let store = store! - try store.save(ShaderPresetData(name: "shader a", shader: "CRT", parameters: [:], id: "id1")) - try store.save(ShaderPresetData(name: "shader b", shader: "CRT", parameters: [:], id: "id1")) - expect(store.findPreset(byName: "shader a")).to(beNil()) - expect(store.findPreset(byName: "shader b")).toNot(beNil()) - } - - func testRenameShaderFailsForDuplicateName() throws { - let store = store! - try store.save(ShaderPresetData(name: "shader a", shader: "CRT", parameters: [:], id: "id1")) - try store.save(ShaderPresetData(name: "shader b", shader: "CRT", parameters: [:], id: "id2")) - - expect { - try store.save(ShaderPresetData(name: "shader b", shader: "CRT", parameters: [:], id: "id1")) - } - .to(throwError(ShaderPresetStorageError.duplicateName)) - } - func testFailsForModifiedShader() { let store = store! expect { diff --git a/Source/ShaderPreset.swift b/Source/ShaderPreset.swift index 02041df..132a05b 100644 --- a/Source/ShaderPreset.swift +++ b/Source/ShaderPreset.swift @@ -29,23 +29,36 @@ import Foundation public var name: String public let shader: OEShaderModel public var parameters: [String: Double] + public let createdAt: Date - public init(name: String, shader: OEShaderModel, parameters: [String: Double]? = nil, id: String? = nil) { + public init(name: String, shader: OEShaderModel, parameters: [String: Double]? = nil, id: String? = nil, createdAt: Date = Date()) { // generate an ID that is useful for us humans, but still unique enough // that a client won't generate duplicates. self.id = id ?? "\(shader.name):\(UInt(Date().timeIntervalSince1970))" self.name = name self.shader = shader self.parameters = parameters ?? Dictionary(allParams: shader.defaultParameters) + self.createdAt = createdAt super.init() } } +extension ShaderPreset: Comparable { + public static func < (lhs: ShaderPreset, rhs: ShaderPreset) -> Bool { + let res = lhs.name.localizedCompare(rhs.name) + if res == .orderedSame { + return lhs.createdAt < rhs.createdAt + } + return res == .orderedAscending + } +} + extension ShaderPresetData { public init(preset: ShaderPreset) { id = preset.id name = preset.name shader = preset.shader.name parameters = preset.parameters + createdAt = preset.createdAt.timeIntervalSince1970 } } diff --git a/Source/ShaderPresetData.swift b/Source/ShaderPresetData.swift index 2bb66ea..b698599 100644 --- a/Source/ShaderPresetData.swift +++ b/Source/ShaderPresetData.swift @@ -29,11 +29,13 @@ public struct ShaderPresetData: Identifiable, Hashable, Codable { public let name: String public let shader: String public var parameters: [String: Double] + public let createdAt: TimeInterval? - public init(name: String, shader: String, parameters: [String: Double], id: String? = nil) { + public init(name: String, shader: String, parameters: [String: Double], id: String? = nil, createdAt: TimeInterval? = nil) { self.id = id ?? name self.name = name self.shader = shader self.parameters = parameters + self.createdAt = createdAt } } diff --git a/Source/ShaderPresetStorage.swift b/Source/ShaderPresetStorage.swift index d71f9ca..2d4cc0f 100644 --- a/Source/ShaderPresetStorage.swift +++ b/Source/ShaderPresetStorage.swift @@ -25,9 +25,6 @@ import Foundation public enum ShaderPresetStorageError: Error { - /// A preset with the same name already exists - case duplicateName - /// The preset shader was changed case shaderModified @@ -42,17 +39,11 @@ public protocol ShaderPresetStorage { /// - Returns: An array of ``ShaderPresetData`` objects. func findPresets(byShader name: String) -> [ShaderPresetData] - /// Returns the shader preset for the specified preset name. - /// - Parameter name: The name of the preset to find. - /// - Returns: A preset matching the specified name. - func findPreset(byName name: String) -> ShaderPresetData? - /// Returns the shader preset for the specified identifier. /// - Parameter id: The identifier of the preset to find. /// - Returns: A preset matching the specified identifier. func findPreset(byID id: String) -> ShaderPresetData? func save(_ preset: ShaderPresetData) throws func remove(_ preset: ShaderPresetData) - func exists(byName name: String) -> Bool func exists(byID id: String) -> Bool } diff --git a/Source/ShaderPresetStore.swift b/Source/ShaderPresetStore.swift index 0f02506..7ff1f94 100644 --- a/Source/ShaderPresetStore.swift +++ b/Source/ShaderPresetStore.swift @@ -45,10 +45,6 @@ public class ShaderPresetStore { // Indices var byID = [String: ShaderPreset]() // map id → preset - var byName = [String: String]() // map name → id - - /// tracks the persisted name of the shader - var idToName = [String: String]() public init(store: ShaderPresetStorage, shaders: OEShaderStore) { self.store = store @@ -87,12 +83,6 @@ public class ShaderPresetStore { if self.byID[data.id] == nil { // new item self.byID[data.id] = preset - self.byName[data.name] = data.id - self.idToName[data.id] = data.name - } else if let name = self.idToName[data.id], name != data.name { - self.byName.removeValue(forKey: name) - self.byName[data.name] = data.id - self.idToName[data.id] = data.name } } } @@ -102,9 +92,6 @@ public class ShaderPresetStore { store.remove(data) queue.async(flags: .barrier) { self.byID.removeValue(forKey: data.id) - if let name = self.idToName.removeValue(forKey: data.id) { - self.byName.removeValue(forKey: name) - } } } @@ -122,33 +109,12 @@ public class ShaderPresetStore { } } - /// Return the shader preset matching the specified name. - /// - /// - Note: - /// This function returns `nil` if a preset is found - /// but no valid shader is installed. - /// - /// - Parameter name: The name of the preset to locate. - /// - Returns: A matching preset or `nil`. - public func findPreset(byName name: String) -> ShaderPreset? { - queue.sync { - getPreset(byName: name) - } - } - public func findPreset(byID id: String) -> ShaderPreset? { queue.sync { getPreset(byID: id) } } - /// Determines if a preset exists with the specified name. - /// - Parameter name: The unique name of the preset to search for. - /// - Returns: `true` if a preset exists. - public func exists(byName name: String) -> Bool { - store.exists(byName: name) - } - /// Determines if a preset exists with the specified id. /// - Parameter id: The unique identifier of the preset to search for. /// - Returns: `true` if a preset exists. @@ -175,24 +141,10 @@ public class ShaderPresetStore { return makePreset(data: data, shader: shader) } - private func getPreset(byName name: String) -> ShaderPreset? { - if let id = byName[name] { - return getPreset(byID: id) - } - - guard - let data = store.findPreset(byName: name), - let shader = shaders[data.shader] - else { return nil } - - return makePreset(data: data, shader: shader) - } - private func makePreset(data: ShaderPresetData, shader: OEShaderModel) -> ShaderPreset { - let preset = ShaderPreset(name: data.name, shader: shader, parameters: data.parameters, id: data.id) - byID[data.id] = preset - byName[data.name] = preset.id - idToName[data.id] = preset.name + let createdAt = data.createdAt != nil ? Date(timeIntervalSince1970: data.createdAt!) : Date() + let preset = ShaderPreset(name: data.name, shader: shader, parameters: data.parameters, id: data.id, createdAt: createdAt) + byID[data.id] = preset return preset } } diff --git a/Source/ShaderPresetTextEncoding.swift b/Source/ShaderPresetTextEncoding.swift index 8b42620..517a2e9 100644 --- a/Source/ShaderPresetTextEncoding.swift +++ b/Source/ShaderPresetTextEncoding.swift @@ -27,6 +27,7 @@ import OpenEmuKitPrivate public enum ShaderPresetWriteError: Error { case invalidCharacters + case missingCreatedAt } @frozen public struct ShaderPresetTextWriter { @@ -39,7 +40,8 @@ public enum ShaderPresetWriteError: Error { public static let name = Self(rawValue: 1 << 0) public static let shader = Self(rawValue: 1 << 1) - public static let sign = Self(rawValue: 1 << 2) + public static let createdAt = Self(rawValue: 1 << 2) + public static let sign = Self(rawValue: 1 << 3) public static let all: Self = [.name, .shader] } @@ -85,6 +87,16 @@ public enum ShaderPresetWriteError: Error { s.append("$shader=\"\(c.shader)\"") } + if options.contains(.createdAt) { + guard let createdAt = c.createdAt else { throw ShaderPresetWriteError.missingCreatedAt } + if !first { + s.append(";") + } else { + first = false + } + s.append("$createdAt=\"\(UInt64(createdAt))\"") + } + // Sort the keys for a consistent output for key in c.parameters.keys.sorted() { if !first { @@ -140,8 +152,9 @@ public enum ShaderPresetReadError: Error { guard let tokens = try? PKVScanner.parse(text: text[.. Bool { - queue.sync { - indexByName[name] != nil - } - } - public func exists(byID id: String) -> Bool { queue.sync { store.string(forKey: Self.makeKey(id)) != nil } } - public func presets(matching predicate: (ShaderPresetData) -> Bool) -> [ShaderPresetData] { - queue.sync { - indexByName.values.compactMap(load(_:)).filter(predicate) - } - } - public func findPresets(byShader name: String) -> [ShaderPresetData] { queue.sync { indexByShader[name]?.compactMap(load(_:)) ?? [] } } - public func findPreset(byName name: String) -> ShaderPresetData? { - queue.sync { - guard let id = indexByName[name] else { return nil } - return load(id) - } - } - public func findPreset(byID id: String) -> ShaderPresetData? { queue.sync { load(id) @@ -94,10 +72,6 @@ public class UserDefaultsPresetStorage: ShaderPresetStorage { public func save(_ preset: ShaderPresetData) throws { try queue.sync(flags: .barrier) { - if let existing = indexByName[preset.name], existing != preset.id { - throw ShaderPresetStorageError.duplicateName - } - let existing = load(preset.id) if let existing = existing, existing.shader != preset.shader { @@ -105,21 +79,18 @@ public class UserDefaultsPresetStorage: ShaderPresetStorage { throw ShaderPresetStorageError.shaderModified } + // Ensure created at is set + let createdAt = existing?.createdAt ?? preset.createdAt ?? Date().timeIntervalSince1970 + let preset = ShaderPresetData(name: preset.name, shader: preset.shader, parameters: preset.parameters, id: preset.id, createdAt: createdAt) + do { - let text = try ShaderPresetTextWriter().write(preset: preset, options: [.name, .shader]) + let text = try ShaderPresetTextWriter().write(preset: preset, options: [.name, .shader, .createdAt]) store.set(text, forKey: Self.makeKey(preset.id)) // // Update indices // - if let existing = existing { - if existing.name != preset.name { - // Preset was renamed - indexByName[existing.name] = nil - indexByName[preset.name] = preset.id - } - } else { - indexByName[preset.name] = preset.id + if existing == nil { var idsByShader = indexByShader[preset.shader] ?? [] idsByShader.append(preset.id) indexByShader[preset.shader] = idsByShader @@ -139,7 +110,6 @@ public class UserDefaultsPresetStorage: ShaderPresetStorage { // // Update indices // - indexByName.removeValue(forKey: existing.name) if var idsByShader = indexByShader[existing.shader], let index = idsByShader.firstIndex(of: existing.id)