spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
1//2// ReaderView.swift3// Zyquo Atlas4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Reader mode: a clean, themeable article view built from the tab's extracted9// PageContext (Readability), with an optional AI summary at the top. Reader10// typography (measure, size, spacing) follows the theme; honest fallback when11// the page isn't an article.12//1314import SwiftUI1516struct ReaderView: View {17 @ObservedObject var tab: Tab18 @EnvironmentObject private var env: AppEnvironment19 @EnvironmentObject private var themeEngine: ThemeEngine20 private var theme: AtlasTheme { themeEngine.theme }2122 @State private var context: PageContext?23 @State private var loadError: String?24 @StateObject private var ai = AIService()2526 var body: some View {27 ScrollView {28 VStack(alignment: .leading, spacing: ZyquoSpacing.md) {29 if let ctx = context {30 Text(ctx.title).font(.system(size: 30 * themeEngine.layout.uiScale, weight: .bold))31 .foregroundStyle(theme.textPrimary)32 if let byline = ctx.byline {33 Text(byline).font(ZyquoFont.caption).foregroundStyle(theme.textSecondary)34 }35 summaryCard(ctx)36 Divider().overlay(theme.border)37 Text(ctx.markdown)38 .font(.system(size: 17 * themeEngine.layout.uiScale))39 .lineSpacing(8)40 .foregroundStyle(theme.textPrimary)41 .textSelection(.enabled)42 } else if let loadError {43 Label(loadError, systemImage: "doc.plaintext")44 .font(ZyquoFont.body()).foregroundStyle(theme.textSecondary)45 } else {46 ProgressView().frame(maxWidth: .infinity).padding(ZyquoSpacing.xxl)47 }48 }49 .frame(maxWidth: 720, alignment: .leading)50 .frame(maxWidth: .infinity)51 .padding(.horizontal, ZyquoSpacing.xxl)52 .padding(.vertical, ZyquoSpacing.xl)53 }54 .background(theme.backgroundColor)55 .task(id: tab.id) { await load() }56 }5758 private func summaryCard(_ ctx: PageContext) -> some View {59 VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {60 HStack {61 Label("AI summary", systemImage: "sparkles").font(ZyquoFont.caption)62 .foregroundStyle(theme.accentIndigo)63 Spacer()64 if !ai.isStreaming && ai.output.isEmpty {65 Button("Summarize", action: { summarize(ctx) })66 .font(ZyquoFont.caption).foregroundStyle(theme.accent)67 }68 }69 if !ai.output.isEmpty {70 Text(ai.output).font(ZyquoFont.body()).foregroundStyle(theme.textPrimary).textSelection(.enabled)71 } else if ai.isStreaming {72 Text("Summarizing…").font(ZyquoFont.caption).foregroundStyle(theme.textSecondary)73 }74 }75 .padding(ZyquoSpacing.md)76 .background(RoundedRectangle(cornerRadius: ZyquoRadius.medium).fill(theme.accentSubtle))77 }7879 private func load() async {80 do { context = try await tab.extractPageContext() }81 catch { loadError = "Reader couldn't extract this page. It may be an app page or blocked." }82 }8384 private func summarize(_ ctx: PageContext) {85 guard let model = env.model(for: .standard),86 let key = try? env.vault.apiKey(for: model.provider) else { return }87 ai.run(.summarize, on: ctx, model: model, apiKey: key)88 }89}90