Implement song project list

This commit is contained in:
Codex
2026-09-13 14:17:38 +02:00
parent 60b4f1b916
commit e28d45ae18
3 changed files with 90 additions and 10 deletions
@@ -5,6 +5,33 @@ public protocol ProjectStore: Sendable {
func save(_ project: SongProject) async throws
func open(id: String) async throws -> SongProject
func loadProjectIDs() async throws -> [String]
func loadProjectList() async throws -> [SongProjectListItem]
}
public struct SongProjectListItem: Codable, Equatable, Identifiable, Sendable {
public let id: String
public var title: String
public var idea: String
public var createdAt: Date
public var updatedAt: Date
public init(id: String, title: String, idea: String, createdAt: Date, updatedAt: Date) {
self.id = id
self.title = title
self.idea = idea
self.createdAt = createdAt
self.updatedAt = updatedAt
}
public init(project: SongProject) {
self.init(
id: project.id,
title: project.title,
idea: project.idea,
createdAt: project.createdAt,
updatedAt: project.updatedAt
)
}
}
public actor LocalSongProjectStore: ProjectStore {
@@ -43,15 +70,23 @@ public actor LocalSongProjectStore: ProjectStore {
}
public func loadProjectIDs() async throws -> [String] {
try await loadProjectList().map(\.id)
}
public func loadProjectList() async throws -> [SongProjectListItem] {
try ensureDirectoryExists()
let fileURLs = try FileManager.default.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: nil
)
return fileURLs
.filter { $0.pathExtension == Self.fileExtension }
.map { $0.deletingPathExtension().lastPathComponent }
.sorted()
return try projectFileURLs()
.map { url in
let data = try Data(contentsOf: url)
let project = try decoder.decode(SongProject.self, from: data)
return SongProjectListItem(project: project)
}
.sorted { left, right in
if left.updatedAt == right.updatedAt {
return left.title.localizedStandardCompare(right.title) == .orderedAscending
}
return left.updatedAt > right.updatedAt
}
}
private func ensureDirectoryExists() throws {
@@ -66,6 +101,14 @@ public actor LocalSongProjectStore: ProjectStore {
try data.write(to: url, options: [.atomic])
}
private func projectFileURLs() throws -> [URL] {
try FileManager.default.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: nil
)
.filter { $0.pathExtension == Self.fileExtension }
}
private func projectURL(for id: String) -> URL {
directoryURL
.appendingPathComponent(id)
@@ -20,11 +20,15 @@ final class LocalSongProjectStoreTests: XCTestCase {
func testCreatesOpensSavesAndListsSongProjectsLocally() async throws {
let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL)
let createdAt = Date(timeIntervalSince1970: 100)
let updatedAt = Date(timeIntervalSince1970: 200)
var project = SongProject(
id: "local-project",
title: "Local Project",
idea: "Persist this song locally",
genres: [GenreStyle(id: "pop", name: "Pop")]
genres: [GenreStyle(id: "pop", name: "Pop")],
createdAt: createdAt,
updatedAt: updatedAt
)
try await store.create(project)
@@ -44,6 +48,39 @@ final class LocalSongProjectStoreTests: XCTestCase {
XCTAssertEqual(updatedProject, project)
}
func testLoadsProjectListSortedByMostRecentUpdate() async throws {
let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL)
let older = SongProject(
id: "older-project",
title: "Older",
idea: "Created first",
createdAt: Date(timeIntervalSince1970: 100),
updatedAt: Date(timeIntervalSince1970: 150)
)
let newer = SongProject(
id: "newer-project",
title: "Newer",
idea: "Updated later",
createdAt: Date(timeIntervalSince1970: 200),
updatedAt: Date(timeIntervalSince1970: 300)
)
try await store.create(older)
try await store.create(newer)
let projectList = try await store.loadProjectList()
let projectIDs = try await store.loadProjectIDs()
XCTAssertEqual(
projectList,
[
SongProjectListItem(project: newer),
SongProjectListItem(project: older)
]
)
XCTAssertEqual(projectIDs, ["newer-project", "older-project"])
}
func testCreateDoesNotOverwriteExistingProject() async throws {
let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL)
let project = SongProject(
+1 -1
View File
@@ -33,7 +33,7 @@ requirement is missing and blocks implementation, record it in
## Phase 2 --- Project Persistence & Versions
- [x] Create new/open/save song projects locally.
- [ ] Implement project list.
- [x] Implement project list.
- [ ] Implement immutable or snapshot-based version history.
- [ ] Restore a previous version without destroying later versions.
- [ ] Add autosave strategy that does not create excessive versions.