import SwiftUI struct BookmarkRow: View { let bookmark: Bookmark var readingProgress: Double = 0 var onPodcast: (() -> Void)? = nil @State private var podcastTapCount = 0 var body: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top, spacing: 11) { FaviconView(url: bookmark.faviconUrl) .overlay(alignment: .topLeading) { if bookmark.unread { Circle() .fill(.blue) .frame(width: 8, height: 8) .offset(x: -3, y: -3) } } .padding(.top, 1) VStack(alignment: .leading, spacing: 4) { Text(bookmark.displayTitle) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(.primary) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) HStack(spacing: 4) { Text(bookmark.domain) Text("ยท") Text(bookmark.dateAdded, style: .relative) } .font(.system(size: 13)) .foregroundStyle(.secondary) } Spacer() if let onPodcast { Button { podcastTapCount += 1 onPodcast() } label: { Image(systemName: podcastCached ? "headphones.circle.fill" : "headphones.circle") .font(.system(size: 20)) .foregroundStyle(podcastCached ? .blue : Color(.systemGray3)) } .buttonStyle(.plain) .padding(.top, 1) .sensoryFeedback(.impact(weight: .medium), trigger: podcastTapCount) } } if let summary = bookmark.aiSummary { Text(summary) .font(.system(size: 14)) .italic() .foregroundStyle(.secondary) .lineLimit(2) .padding(.leading, 44) } if !effectiveTags.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { ForEach(effectiveTags, id: \.self) { tag in Text(tag) .font(.system(size: 12, weight: .medium)) .foregroundStyle(.secondary) .padding(.horizontal, 8) .padding(.vertical, 3) .background(Color(.systemGray6)) .clipShape(Capsule()) } } } .padding(.leading, 44) } } .padding(.vertical, 14) .contentShape(Rectangle()) .overlay(alignment: .bottom) { if readingProgress > 0.02 { GeometryReader { geo in ZStack(alignment: .leading) { Rectangle() .fill(Color(.systemGray5)) Rectangle() .fill(Color(.systemGreen)) .frame(width: geo.size.width * min(readingProgress, 1)) } } .frame(height: 2) } } } private var podcastCached: Bool { FileManager.default.fileExists(atPath: ClaudeService.cachedPodcastURL(for: bookmark.url).path) } private var effectiveTags: [String] { let base = bookmark.tagNames let ai = bookmark.aiTags ?? [] let extra = ai.filter { !base.contains($0) }.prefix(3) return (base + extra).prefix(6).map { $0 } } } struct FaviconView: View { let url: String? var body: some View { Group { if let urlString = url, let faviconUrl = URL(string: urlString) { AsyncImage(url: faviconUrl) { phase in switch phase { case .success(let image): image.resizable().scaledToFit() default: placeholder } } } else { placeholder } } .frame(width: 32, height: 32) .clipShape(RoundedRectangle(cornerRadius: 7)) } private var placeholder: some View { RoundedRectangle(cornerRadius: 6) .fill(Color(.systemGray5)) .overlay { Image(systemName: "bookmark") .font(.system(size: 11, weight: .medium)) .foregroundStyle(Color(.systemGray2)) } } }