// // LibraryView.swift // Prisme // // Author: Simon-Pierre Boucher // import SwiftUI /// The library: saved excerpts and structured favourites. Content saved /// here is native data with a source anchor — not a list of rotting URLs. struct LibraryView: View { let library: LibraryStore let containers: IdentityContainerStore let onOpen: (URL, IdentityContainer.ID) -> Void @Environment(\.dismiss) private var dismiss @State private var section: Section = .excerpts private static let generated = Color(red: 0.55, green: 0.36, blue: 0.96) enum Section: String, CaseIterable { case excerpts = "Extraits" case pages = "Pages" } var body: some View { NavigationStack { VStack(spacing: 0) { Picker("Section", selection: $section) { ForEach(Section.allCases, id: \.self) { section in Text(section.rawValue).tag(section) } } .pickerStyle(.segmented) .padding(.horizontal, Spacing.l) .padding(.bottom, Spacing.s) switch section { case .excerpts: excerptList case .pages: favoriteList } } .navigationTitle("Bibliothèque") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Fermer") { dismiss() } } } } } // MARK: - Excerpts @ViewBuilder private var excerptList: some View { let excerpts = library.excerpts if excerpts.isEmpty { ContentUnavailableView { Label("Aucun extrait", systemImage: "quote.opening") } description: { Text("Sélectionnez un passage dans une page, puis « Sauver l'extrait ». C'est le passage qui est gardé, avec sa source.") } } else { List { ForEach(excerpts, id: \.persistentModelID) { excerpt in excerptRow(excerpt) .swipeActions { Button(role: .destructive) { library.delete(excerpt) } label: { Label("Supprimer", systemImage: "trash") } } } } .listStyle(.plain) } } private func excerptRow(_ excerpt: Excerpt) -> some View { Button { open(excerpt.sourceURLString, containerID: excerpt.containerID) } label: { VStack(alignment: .leading, spacing: Spacing.s) { HStack(alignment: .top, spacing: Spacing.m) { RoundedRectangle(cornerRadius: 2) .fill(color(for: excerpt.containerID)) .frame(width: 3) Text(excerpt.text) .font(.callout) .foregroundStyle(.primary) .lineLimit(5) } HStack(spacing: Spacing.xs) { if let section = excerpt.sectionTitle, !section.isEmpty { Text(section) Text("·") } Text(excerpt.sourceHost) Text("·") Text(excerpt.savedAt, style: .date) } .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } .padding(.vertical, Spacing.xs) } .buttonStyle(.plain) .accessibilityIdentifier("library.excerpt") } // MARK: - Structured favourites @ViewBuilder private var favoriteList: some View { let favorites = library.favorites if favorites.isEmpty { ContentUnavailableView { Label("Aucune page sauvée", systemImage: "bookmark") } description: { Text("Dans le lecteur, touchez le signet : la page est gardée en données natives — l'essentiel, le plan, la source.") } } else { List { ForEach(favorites, id: \.persistentModelID) { favorite in favoriteRow(favorite) .swipeActions { Button(role: .destructive) { library.delete(favorite) } label: { Label("Supprimer", systemImage: "trash") } } } } .listStyle(.plain) } } private func favoriteRow(_ favorite: StructuredFavorite) -> some View { Button { open(favorite.urlString, containerID: favorite.containerID) } label: { VStack(alignment: .leading, spacing: Spacing.xs) { HStack(spacing: Spacing.s) { Circle() .fill(color(for: favorite.containerID)) .frame(width: 8, height: 8) Text(favorite.title) .foregroundStyle(.primary) .lineLimit(2) } if !favorite.gist.isEmpty { HStack(alignment: .top, spacing: Spacing.xs) { if favorite.isGenerated { Image(systemName: "sparkles") .font(.caption2) .foregroundStyle(Self.generated) .padding(.top, 2) } Text(favorite.gist) .font(.caption) .foregroundStyle(favorite.isGenerated ? AnyShapeStyle(Self.generated) : AnyShapeStyle(.secondary)) .italic(favorite.isGenerated) .lineLimit(2) } } HStack(spacing: Spacing.xs) { Text(favorite.host) Text("·") let count = favorite.outlineTitles.count Text(count > 1 ? "\(count) sections" : "\(count) section") Text("·") Text(favorite.savedAt, style: .date) } .font(.caption2) .foregroundStyle(.tertiary) .lineLimit(1) } .padding(.vertical, Spacing.xs) } .buttonStyle(.plain) .accessibilityIdentifier("library.favorite") } // MARK: - Helpers private func open(_ urlString: String, containerID: UUID) { guard let url = URL(string: urlString) else { return } onOpen(url, containerID) dismiss() } private func color(for containerID: UUID) -> Color { containers.container(for: containerID)?.color.color ?? .secondary } }