import Foundation
import SwiftUI

struct Book: Codable, Identifiable {
    let id: UUID
    let title: String
    let filename: String
    let fileExtension: String
}

final class LibraryStore: ObservableObject {
    @Published private(set) var books: [Book] = []
    @Published var errorMessage: String?

    private let fileManager = FileManager.default
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    private var documentsURL: URL {
        fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }

    private var indexURL: URL {
        documentsURL.appendingPathComponent("library.json")
    }

    init() {
        do {
            try fileManager.createDirectory(at: documentsURL, withIntermediateDirectories: true)
            if fileManager.fileExists(atPath: indexURL.path) {
                books = try decoder.decode([Book].self, from: Data(contentsOf: indexURL))
            }
        } catch {
            errorMessage = "Impossible de charger la bibliothèque : \(error.localizedDescription)"
        }
    }

    func importBooks(from urls: [URL]) {
        var imported: [Book] = []

        do {
            for url in urls {
                imported.append(try copyBook(from: url))
            }

            let updatedBooks = books + imported
            try encoder.encode(updatedBooks).write(to: indexURL, options: .atomic)
            books = updatedBooks
        } catch {
            var cleanupErrors: [String] = []
            for book in imported {
                do {
                    try fileManager.removeItem(at: documentsURL.appendingPathComponent(book.filename))
                } catch {
                    cleanupErrors.append(error.localizedDescription)
                }
            }
            let cleanupMessage = cleanupErrors.isEmpty
                ? ""
                : " Certains fichiers temporaires n’ont pas pu être supprimés."
            errorMessage = "Impossible d’importer le document : \(error.localizedDescription)\(cleanupMessage)"
        }
    }

    func deleteBooks(at offsets: IndexSet) {
        let removedBooks = offsets.map { books[$0] }
        let updatedBooks = books.enumerated()
            .filter { !offsets.contains($0.offset) }
            .map(\.element)

        do {
            try encoder.encode(updatedBooks).write(to: indexURL, options: .atomic)
            books = updatedBooks
            for book in removedBooks {
                try fileManager.removeItem(at: documentsURL.appendingPathComponent(book.filename))
            }
        } catch {
            errorMessage = "Impossible de supprimer le document : \(error.localizedDescription)"
        }
    }

    func fileURL(for book: Book) -> URL {
        documentsURL.appendingPathComponent(book.filename)
    }

    private func copyBook(from sourceURL: URL) throws -> Book {
        let hasAccess = sourceURL.startAccessingSecurityScopedResource()
        defer {
            if hasAccess {
                sourceURL.stopAccessingSecurityScopedResource()
            }
        }

        let fileExtension = sourceURL.pathExtension.lowercased()

        let id = UUID()
        let filename = "\(id.uuidString).\(fileExtension)"
        try fileManager.copyItem(at: sourceURL, to: documentsURL.appendingPathComponent(filename))

        let title = sourceURL.deletingPathExtension().lastPathComponent
        return Book(id: id, title: title, filename: filename, fileExtension: fileExtension)
    }
}

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: