SPB Git

spb/prisme Public MIT

Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.

Swift 96.7% JavaScript 3.3%
7.0 KB · 201 lines swift
Raw Blame History
1//2//  LibraryView.swift3//  Prisme4//5//  Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import SwiftUI910/// The library: saved excerpts and structured favourites. Content saved11/// here is native data with a source anchor — not a list of rotting URLs.12struct LibraryView: View {13    let library: LibraryStore14    let containers: IdentityContainerStore15    let onOpen: (URL, IdentityContainer.ID) -> Void1617    @Environment(\.dismiss) private var dismiss18    @State private var section: Section = .excerpts1920    private static let generated = Color(red: 0.55, green: 0.36, blue: 0.96)2122    enum Section: String, CaseIterable {23        case excerpts = "Extraits"24        case pages = "Pages"25    }2627    var body: some View {28        NavigationStack {29            VStack(spacing: 0) {30                Picker("Section", selection: $section) {31                    ForEach(Section.allCases, id: \.self) { section in32                        Text(section.rawValue).tag(section)33                    }34                }35                .pickerStyle(.segmented)36                .padding(.horizontal, Spacing.l)37                .padding(.bottom, Spacing.s)3839                switch section {40                case .excerpts: excerptList41                case .pages: favoriteList42                }43            }44            .navigationTitle("Bibliothèque")45            .navigationBarTitleDisplayMode(.inline)46            .toolbar {47                ToolbarItem(placement: .cancellationAction) {48                    Button("Fermer") { dismiss() }49                }50            }51        }52    }5354    // MARK: - Excerpts5556    @ViewBuilder57    private var excerptList: some View {58        let excerpts = library.excerpts59        if excerpts.isEmpty {60            ContentUnavailableView {61                Label("Aucun extrait", systemImage: "quote.opening")62            } description: {63                Text("Sélectionnez un passage dans une page, puis « Sauver l'extrait ». C'est le passage qui est gardé, avec sa source.")64            }65        } else {66            List {67                ForEach(excerpts, id: \.persistentModelID) { excerpt in68                    excerptRow(excerpt)69                        .swipeActions {70                            Button(role: .destructive) {71                                library.delete(excerpt)72                            } label: {73                                Label("Supprimer", systemImage: "trash")74                            }75                        }76                }77            }78            .listStyle(.plain)79        }80    }8182    private func excerptRow(_ excerpt: Excerpt) -> some View {83        Button {84            open(excerpt.sourceURLString, containerID: excerpt.containerID)85        } label: {86            VStack(alignment: .leading, spacing: Spacing.s) {87                HStack(alignment: .top, spacing: Spacing.m) {88                    RoundedRectangle(cornerRadius: 2)89                        .fill(color(for: excerpt.containerID))90                        .frame(width: 3)91                    Text(excerpt.text)92                        .font(.callout)93                        .foregroundStyle(.primary)94                        .lineLimit(5)95                }96                HStack(spacing: Spacing.xs) {97                    if let section = excerpt.sectionTitle, !section.isEmpty {98                        Text(section)99                        Text("·")100                    }101                    Text(excerpt.sourceHost)102                    Text("·")103                    Text(excerpt.savedAt, style: .date)104                }105                .font(.caption)106                .foregroundStyle(.secondary)107                .lineLimit(1)108            }109            .padding(.vertical, Spacing.xs)110        }111        .buttonStyle(.plain)112        .accessibilityIdentifier("library.excerpt")113    }114115    // MARK: - Structured favourites116117    @ViewBuilder118    private var favoriteList: some View {119        let favorites = library.favorites120        if favorites.isEmpty {121            ContentUnavailableView {122                Label("Aucune page sauvée", systemImage: "bookmark")123            } description: {124                Text("Dans le lecteur, touchez le signet : la page est gardée en données natives — l'essentiel, le plan, la source.")125            }126        } else {127            List {128                ForEach(favorites, id: \.persistentModelID) { favorite in129                    favoriteRow(favorite)130                        .swipeActions {131                            Button(role: .destructive) {132                                library.delete(favorite)133                            } label: {134                                Label("Supprimer", systemImage: "trash")135                            }136                        }137                }138            }139            .listStyle(.plain)140        }141    }142143    private func favoriteRow(_ favorite: StructuredFavorite) -> some View {144        Button {145            open(favorite.urlString, containerID: favorite.containerID)146        } label: {147            VStack(alignment: .leading, spacing: Spacing.xs) {148                HStack(spacing: Spacing.s) {149                    Circle()150                        .fill(color(for: favorite.containerID))151                        .frame(width: 8, height: 8)152                    Text(favorite.title)153                        .foregroundStyle(.primary)154                        .lineLimit(2)155                }156                if !favorite.gist.isEmpty {157                    HStack(alignment: .top, spacing: Spacing.xs) {158                        if favorite.isGenerated {159                            Image(systemName: "sparkles")160                                .font(.caption2)161                                .foregroundStyle(Self.generated)162                                .padding(.top, 2)163                        }164                        Text(favorite.gist)165                            .font(.caption)166                            .foregroundStyle(favorite.isGenerated ? AnyShapeStyle(Self.generated) : AnyShapeStyle(.secondary))167                            .italic(favorite.isGenerated)168                            .lineLimit(2)169                    }170                }171                HStack(spacing: Spacing.xs) {172                    Text(favorite.host)173                    Text("·")174                    let count = favorite.outlineTitles.count175                    Text(count > 1 ? "\(count) sections" : "\(count) section")176                    Text("·")177                    Text(favorite.savedAt, style: .date)178                }179                .font(.caption2)180                .foregroundStyle(.tertiary)181                .lineLimit(1)182            }183            .padding(.vertical, Spacing.xs)184        }185        .buttonStyle(.plain)186        .accessibilityIdentifier("library.favorite")187    }188189    // MARK: - Helpers190191    private func open(_ urlString: String, containerID: UUID) {192        guard let url = URL(string: urlString) else { return }193        onOpen(url, containerID)194        dismiss()195    }196197    private func color(for containerID: UUID) -> Color {198        containers.container(for: containerID)?.color.color ?? .secondary199    }200}201