add SwiftData storage with BookStore for library management, position tracking
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ let package = Package(
|
||||
products: [
|
||||
.library(name: "VorleserKit", targets: ["VorleserKit"]),
|
||||
.library(name: "BookParser", targets: ["BookParser"]),
|
||||
.library(name: "Storage", targets: ["Storage"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0"),
|
||||
@@ -18,12 +19,20 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "VorleserKit",
|
||||
dependencies: ["Storage"]
|
||||
),
|
||||
.target(
|
||||
name: "Storage",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "BookParser",
|
||||
dependencies: ["VorleserKit", "ZIPFoundation", "SwiftSoup"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "StorageTests",
|
||||
dependencies: ["Storage"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "BookParserTests",
|
||||
dependencies: ["BookParser"],
|
||||
|
||||
57
VorleserKit/Sources/Storage/BookStore.swift
Normal file
57
VorleserKit/Sources/Storage/BookStore.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
public struct BookStore {
|
||||
private let modelContainer: ModelContainer
|
||||
private let documentsDirectory: URL
|
||||
|
||||
public init(modelContainer: ModelContainer, documentsDirectory: URL) {
|
||||
self.modelContainer = modelContainer
|
||||
self.documentsDirectory = documentsDirectory
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func importBook(from sourceURL: URL, title: String, author: String?) throws -> StoredBook {
|
||||
let fileName = "\(UUID().uuidString)_\(sourceURL.lastPathComponent)"
|
||||
let destination = documentsDirectory.appendingPathComponent(fileName)
|
||||
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
||||
|
||||
let stored = StoredBook(title: title, author: author, sourceFileName: fileName)
|
||||
let context = modelContainer.mainContext
|
||||
context.insert(stored)
|
||||
try context.save()
|
||||
return stored
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func allBooks() throws -> [StoredBook] {
|
||||
let context = modelContainer.mainContext
|
||||
let descriptor = FetchDescriptor<StoredBook>(
|
||||
sortBy: [SortDescriptor(\.lastRead, order: .reverse), SortDescriptor(\.dateAdded, order: .reverse)]
|
||||
)
|
||||
return try context.fetch(descriptor)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func updatePosition(_ book: StoredBook, position: Int) throws {
|
||||
book.lastPosition = position
|
||||
book.lastRead = .now
|
||||
try modelContainer.mainContext.save()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func deleteBook(_ book: StoredBook) throws {
|
||||
let filePath = documentsDirectory.appendingPathComponent(book.sourceFileName)
|
||||
try? FileManager.default.removeItem(at: filePath)
|
||||
modelContainer.mainContext.delete(book)
|
||||
try modelContainer.mainContext.save()
|
||||
}
|
||||
|
||||
public func fileURL(for book: StoredBook) -> URL {
|
||||
documentsDirectory.appendingPathComponent(book.sourceFileName)
|
||||
}
|
||||
|
||||
public func fileExists(for book: StoredBook) -> Bool {
|
||||
FileManager.default.fileExists(atPath: fileURL(for: book).path)
|
||||
}
|
||||
}
|
||||
34
VorleserKit/Sources/Storage/StoredBook.swift
Normal file
34
VorleserKit/Sources/Storage/StoredBook.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
public class StoredBook {
|
||||
public var bookID: UUID
|
||||
public var title: String
|
||||
public var author: String?
|
||||
public var sourceFileName: String
|
||||
public var dateAdded: Date
|
||||
public var lastPosition: Int
|
||||
public var lastRead: Date?
|
||||
public var voiceName: String?
|
||||
|
||||
public init(
|
||||
bookID: UUID = UUID(),
|
||||
title: String,
|
||||
author: String? = nil,
|
||||
sourceFileName: String,
|
||||
dateAdded: Date = .now,
|
||||
lastPosition: Int = 0,
|
||||
lastRead: Date? = nil,
|
||||
voiceName: String? = nil
|
||||
) {
|
||||
self.bookID = bookID
|
||||
self.title = title
|
||||
self.author = author
|
||||
self.sourceFileName = sourceFileName
|
||||
self.dateAdded = dateAdded
|
||||
self.lastPosition = lastPosition
|
||||
self.lastRead = lastRead
|
||||
self.voiceName = voiceName
|
||||
}
|
||||
}
|
||||
61
VorleserKit/Tests/StorageTests/BookStoreTests.swift
Normal file
61
VorleserKit/Tests/StorageTests/BookStoreTests.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
import SwiftData
|
||||
@testable import Storage
|
||||
|
||||
@Suite("BookStore")
|
||||
struct BookStoreTests {
|
||||
@MainActor
|
||||
func makeStore() throws -> (BookStore, URL) {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: StoredBook.self, configurations: config)
|
||||
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true)
|
||||
return (BookStore(modelContainer: container, documentsDirectory: tmpDir), tmpDir)
|
||||
}
|
||||
|
||||
@Test @MainActor func importAndList() throws {
|
||||
let (store, tmpDir) = try makeStore()
|
||||
defer { try? FileManager.default.removeItem(at: tmpDir) }
|
||||
|
||||
let sourceFile = tmpDir.appendingPathComponent("source.epub")
|
||||
try "fake epub".write(to: sourceFile, atomically: true, encoding: .utf8)
|
||||
|
||||
let stored = try store.importBook(from: sourceFile, title: "Test Book", author: "Author")
|
||||
#expect(stored.title == "Test Book")
|
||||
#expect(stored.lastPosition == 0)
|
||||
|
||||
let books = try store.allBooks()
|
||||
#expect(books.count == 1)
|
||||
#expect(store.fileExists(for: books[0]))
|
||||
}
|
||||
|
||||
@Test @MainActor func updatePosition() throws {
|
||||
let (store, tmpDir) = try makeStore()
|
||||
defer { try? FileManager.default.removeItem(at: tmpDir) }
|
||||
|
||||
let sourceFile = tmpDir.appendingPathComponent("source.txt")
|
||||
try "text".write(to: sourceFile, atomically: true, encoding: .utf8)
|
||||
|
||||
let stored = try store.importBook(from: sourceFile, title: "Book", author: nil)
|
||||
try store.updatePosition(stored, position: 500)
|
||||
#expect(stored.lastPosition == 500)
|
||||
#expect(stored.lastRead != nil)
|
||||
}
|
||||
|
||||
@Test @MainActor func deleteBook() throws {
|
||||
let (store, tmpDir) = try makeStore()
|
||||
defer { try? FileManager.default.removeItem(at: tmpDir) }
|
||||
|
||||
let sourceFile = tmpDir.appendingPathComponent("source.txt")
|
||||
try "text".write(to: sourceFile, atomically: true, encoding: .utf8)
|
||||
|
||||
let stored = try store.importBook(from: sourceFile, title: "Book", author: nil)
|
||||
#expect(store.fileExists(for: stored))
|
||||
|
||||
try store.deleteBook(stored)
|
||||
let books = try store.allBooks()
|
||||
#expect(books.isEmpty)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user