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%
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