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 save(_ project: SongProject) async throws
func open(id: String) async throws -> SongProject func open(id: String) async throws -> SongProject
func loadProjectIDs() async throws -> [String] 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 { public actor LocalSongProjectStore: ProjectStore {
@@ -43,15 +70,23 @@ public actor LocalSongProjectStore: ProjectStore {
} }
public func loadProjectIDs() async throws -> [String] { public func loadProjectIDs() async throws -> [String] {
try await loadProjectList().map(\.id)
}
public func loadProjectList() async throws -> [SongProjectListItem] {
try ensureDirectoryExists() try ensureDirectoryExists()
let fileURLs = try FileManager.default.contentsOfDirectory( return try projectFileURLs()
at: directoryURL, .map { url in
includingPropertiesForKeys: nil let data = try Data(contentsOf: url)
) let project = try decoder.decode(SongProject.self, from: data)
return fileURLs return SongProjectListItem(project: project)
.filter { $0.pathExtension == Self.fileExtension } }
.map { $0.deletingPathExtension().lastPathComponent } .sorted { left, right in
.sorted() if left.updatedAt == right.updatedAt {
return left.title.localizedStandardCompare(right.title) == .orderedAscending
}
return left.updatedAt > right.updatedAt
}
} }
private func ensureDirectoryExists() throws { private func ensureDirectoryExists() throws {
@@ -66,6 +101,14 @@ public actor LocalSongProjectStore: ProjectStore {
try data.write(to: url, options: [.atomic]) 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 { private func projectURL(for id: String) -> URL {
directoryURL directoryURL
.appendingPathComponent(id) .appendingPathComponent(id)
@@ -20,11 +20,15 @@ final class LocalSongProjectStoreTests: XCTestCase {
func testCreatesOpensSavesAndListsSongProjectsLocally() async throws { func testCreatesOpensSavesAndListsSongProjectsLocally() async throws {
let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL)
let createdAt = Date(timeIntervalSince1970: 100)
let updatedAt = Date(timeIntervalSince1970: 200)
var project = SongProject( var project = SongProject(
id: "local-project", id: "local-project",
title: "Local Project", title: "Local Project",
idea: "Persist this song locally", 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) try await store.create(project)
@@ -44,6 +48,39 @@ final class LocalSongProjectStoreTests: XCTestCase {
XCTAssertEqual(updatedProject, project) 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 { func testCreateDoesNotOverwriteExistingProject() async throws {
let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL) let store = LocalSongProjectStore(directoryURL: temporaryDirectoryURL)
let project = SongProject( 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 ## Phase 2 --- Project Persistence & Versions
- [x] Create new/open/save song projects locally. - [x] Create new/open/save song projects locally.
- [ ] Implement project list. - [x] Implement project list.
- [ ] Implement immutable or snapshot-based version history. - [ ] Implement immutable or snapshot-based version history.
- [ ] Restore a previous version without destroying later versions. - [ ] Restore a previous version without destroying later versions.
- [ ] Add autosave strategy that does not create excessive versions. - [ ] Add autosave strategy that does not create excessive versions.