2026-06-29 23:42:39 -07:00
|
|
|
import AppKit
|
2026-06-11 18:12:13 -07:00
|
|
|
import SwiftUI
|
2026-06-24 17:51:26 -07:00
|
|
|
import UniformTypeIdentifiers
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
struct MainView: View {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
2026-06-29 23:42:39 -07:00
|
|
|
@State private var leftSidebarWidth: CGFloat = ReaderAdaptiveLayout(sizeClass: .regular).leftSidebarIdealWidth
|
|
|
|
|
@State private var rightSidebarWidth: CGFloat = ReaderAdaptiveLayout(sizeClass: .regular).rightSidebarIdealWidth
|
|
|
|
|
@State private var leftSidebarDragStartWidth: CGFloat?
|
|
|
|
|
@State private var rightSidebarDragStartWidth: CGFloat?
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
GeometryReader { proxy in
|
2026-06-29 23:42:39 -07:00
|
|
|
content(availableWidth: proxy.size.width)
|
2026-06-11 18:12:13 -07:00
|
|
|
.onAppear {
|
|
|
|
|
appState.updateWindowWidth(proxy.size.width)
|
|
|
|
|
}
|
|
|
|
|
.onChange(of: proxy.size.width) { width in
|
|
|
|
|
appState.updateWindowWidth(width)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationTitle(appState.displayTitle)
|
2026-06-29 23:42:39 -07:00
|
|
|
.frame(
|
|
|
|
|
minWidth: ReaderAdaptiveLayout.minimumWindowWidth,
|
|
|
|
|
minHeight: ReaderAdaptiveLayout.minimumWindowHeight
|
|
|
|
|
)
|
2026-06-11 18:12:13 -07:00
|
|
|
.toolbar {
|
|
|
|
|
ReaderToolbar()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private func content(availableWidth: CGFloat) -> some View {
|
|
|
|
|
let layout = ReaderAdaptiveLayout(width: availableWidth)
|
|
|
|
|
let showsRightSidebar = appState.showCommentsSidebar
|
|
|
|
|
let showsLeftSidebar = appState.showLeftSidebar && (layout.allowsDualSidebars || !showsRightSidebar)
|
|
|
|
|
let sidebarWidths = layout.resolvedSidebarWidths(
|
|
|
|
|
availableWidth: availableWidth,
|
|
|
|
|
requestedLeft: leftSidebarWidth,
|
|
|
|
|
requestedRight: rightSidebarWidth,
|
|
|
|
|
showLeft: showsLeftSidebar,
|
|
|
|
|
showRight: showsRightSidebar
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return VStack(spacing: 0) {
|
2026-06-11 18:12:13 -07:00
|
|
|
if appState.document == nil {
|
|
|
|
|
EmptyDocumentView()
|
|
|
|
|
} else {
|
2026-06-29 23:42:39 -07:00
|
|
|
VStack(spacing: 0) {
|
|
|
|
|
HStack(spacing: 0) {
|
|
|
|
|
if showsLeftSidebar {
|
|
|
|
|
LeftSidebarView()
|
|
|
|
|
.frame(width: sidebarWidths.left)
|
|
|
|
|
.transition(
|
|
|
|
|
.asymmetric(
|
|
|
|
|
insertion: .move(edge: .leading).combined(with: .opacity),
|
|
|
|
|
removal: .opacity
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
SidebarResizeHandle()
|
|
|
|
|
.gesture(leftSidebarResizeGesture(for: layout))
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
PDFReaderView()
|
|
|
|
|
.frame(minWidth: layout.documentMinWidth, maxWidth: .infinity)
|
|
|
|
|
|
|
|
|
|
if showsRightSidebar {
|
|
|
|
|
SidebarResizeHandle()
|
|
|
|
|
.gesture(rightSidebarResizeGesture(for: layout))
|
|
|
|
|
|
|
|
|
|
RightSidebarView()
|
|
|
|
|
.frame(width: sidebarWidths.right)
|
|
|
|
|
.transition(
|
|
|
|
|
.asymmetric(
|
|
|
|
|
insertion: .move(edge: .trailing).combined(with: .opacity),
|
|
|
|
|
removal: .opacity
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.animation(.easeInOut(duration: 0.18), value: appState.showLeftSidebar)
|
|
|
|
|
.animation(.easeInOut(duration: 0.18), value: appState.showCommentsSidebar)
|
|
|
|
|
.animation(.easeInOut(duration: 0.18), value: appState.readerSizeClass)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StatusBarView()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
|
|
|
|
|
private func leftSidebarResizeGesture(for layout: ReaderAdaptiveLayout) -> some Gesture {
|
|
|
|
|
DragGesture(minimumDistance: 0)
|
|
|
|
|
.onChanged { value in
|
|
|
|
|
if leftSidebarDragStartWidth == nil {
|
|
|
|
|
leftSidebarDragStartWidth = leftSidebarWidth
|
|
|
|
|
}
|
|
|
|
|
let proposedWidth = (leftSidebarDragStartWidth ?? leftSidebarWidth) + value.translation.width
|
|
|
|
|
leftSidebarWidth = layout.clampedLeftWidth(proposedWidth)
|
|
|
|
|
}
|
|
|
|
|
.onEnded { _ in
|
|
|
|
|
leftSidebarDragStartWidth = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func rightSidebarResizeGesture(for layout: ReaderAdaptiveLayout) -> some Gesture {
|
|
|
|
|
DragGesture(minimumDistance: 0)
|
|
|
|
|
.onChanged { value in
|
|
|
|
|
if rightSidebarDragStartWidth == nil {
|
|
|
|
|
rightSidebarDragStartWidth = rightSidebarWidth
|
|
|
|
|
}
|
|
|
|
|
let proposedWidth = (rightSidebarDragStartWidth ?? rightSidebarWidth) - value.translation.width
|
|
|
|
|
rightSidebarWidth = layout.clampedRightWidth(proposedWidth)
|
|
|
|
|
}
|
|
|
|
|
.onEnded { _ in
|
|
|
|
|
rightSidebarDragStartWidth = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct SidebarResizeHandle: View {
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
|
@State private var isHovering = false
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Rectangle()
|
|
|
|
|
.fill(Color.clear)
|
|
|
|
|
.frame(width: ReaderAdaptiveLayout.resizeHandleWidth)
|
|
|
|
|
.frame(maxHeight: .infinity)
|
|
|
|
|
.overlay(alignment: .center) {
|
|
|
|
|
Capsule()
|
|
|
|
|
.fill(InterfacePalette.hairline(for: colorScheme).opacity(isHovering ? 0.95 : 0.58))
|
|
|
|
|
.frame(width: isHovering ? 2 : 1, height: isHovering ? 42 : 30)
|
|
|
|
|
.animation(.easeInOut(duration: 0.12), value: isHovering)
|
|
|
|
|
}
|
|
|
|
|
.background {
|
|
|
|
|
Rectangle()
|
|
|
|
|
.fill(Color.accentColor.opacity(isHovering ? 0.05 : 0))
|
|
|
|
|
}
|
|
|
|
|
.contentShape(Rectangle())
|
|
|
|
|
.onHover { hovering in
|
|
|
|
|
isHovering = hovering
|
|
|
|
|
if hovering {
|
|
|
|
|
NSCursor.resizeLeftRight.push()
|
|
|
|
|
} else {
|
|
|
|
|
NSCursor.pop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.onDisappear {
|
|
|
|
|
if isHovering {
|
|
|
|
|
NSCursor.pop()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.help("Resize Sidebar")
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct PDFReaderView: View {
|
2026-06-29 23:42:39 -07:00
|
|
|
@EnvironmentObject private var appState: AppState
|
|
|
|
|
@State private var isDropTargeted = false
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
ZStack(alignment: .bottom) {
|
|
|
|
|
PDFKitRepresentedView()
|
|
|
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
|
|
|
|
|
|
|
|
if appState.canShowSelectionActions {
|
|
|
|
|
SelectionActionBar()
|
|
|
|
|
.padding(.bottom, 14)
|
|
|
|
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isDropTargeted {
|
|
|
|
|
DropTargetOverlay()
|
|
|
|
|
.transition(.opacity.combined(with: .scale(scale: 0.985)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.onDrop(
|
|
|
|
|
of: [UTType.fileURL.identifier],
|
|
|
|
|
isTargeted: $isDropTargeted
|
|
|
|
|
) { providers in
|
|
|
|
|
appState.openDroppedDocument(from: providers)
|
|
|
|
|
}
|
|
|
|
|
.animation(.easeInOut(duration: 0.14), value: appState.canShowSelectionActions)
|
|
|
|
|
.animation(.easeInOut(duration: 0.16), value: isDropTargeted)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct SelectionActionBar: View {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
|
@AppStorage(AppSettings.highlightColorStorageKey)
|
|
|
|
|
private var storedHighlightColor = AppSettings.defaultHighlightColorStorageValue
|
|
|
|
|
|
|
|
|
|
private var activeHighlightDisplayColor: Color {
|
|
|
|
|
Color(nsColor: AppSettings.displayColor(forHighlightColor: AppSettings.highlightColor(from: storedHighlightColor)))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:12:13 -07:00
|
|
|
var body: some View {
|
2026-06-29 23:42:39 -07:00
|
|
|
HStack(spacing: 2) {
|
|
|
|
|
actionButton(
|
|
|
|
|
"Highlight (H)",
|
|
|
|
|
systemImage: "highlighter",
|
|
|
|
|
foregroundStyle: activeHighlightDisplayColor
|
|
|
|
|
) {
|
|
|
|
|
appState.addHighlight()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionButton(
|
|
|
|
|
"Underline (U)",
|
|
|
|
|
systemImage: "underline",
|
|
|
|
|
foregroundStyle: InterfacePalette.primaryText(for: colorScheme)
|
|
|
|
|
) {
|
|
|
|
|
appState.addUnderline()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actionButton(
|
|
|
|
|
"Comment (C)",
|
|
|
|
|
systemImage: "text.bubble",
|
|
|
|
|
foregroundStyle: InterfacePalette.primaryText(for: colorScheme)
|
|
|
|
|
) {
|
|
|
|
|
appState.addComment()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.controlSize(.small)
|
|
|
|
|
.padding(5)
|
|
|
|
|
.background(.regularMaterial, in: Capsule())
|
|
|
|
|
.overlay {
|
|
|
|
|
Capsule()
|
|
|
|
|
.stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 1)
|
|
|
|
|
}
|
|
|
|
|
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.28 : 0.12), radius: 12, y: 5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func actionButton(
|
|
|
|
|
_ title: String,
|
|
|
|
|
systemImage: String,
|
|
|
|
|
foregroundStyle: Color,
|
|
|
|
|
action: @escaping () -> Void
|
|
|
|
|
) -> some View {
|
|
|
|
|
Button(action: action) {
|
|
|
|
|
Image(systemName: systemImage)
|
|
|
|
|
.font(.system(size: 14, weight: .semibold))
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
.foregroundStyle(foregroundStyle)
|
|
|
|
|
.frame(width: 30, height: 28)
|
|
|
|
|
.contentShape(Rectangle())
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.help(title)
|
|
|
|
|
.accessibilityLabel(title)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct EmptyDocumentView: View {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
2026-06-29 23:42:39 -07:00
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
2026-06-24 17:51:26 -07:00
|
|
|
@State private var isDropTargeted = false
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
var body: some View {
|
2026-06-29 23:42:39 -07:00
|
|
|
ZStack {
|
|
|
|
|
VStack(spacing: 24) {
|
|
|
|
|
VStack(spacing: 12) {
|
|
|
|
|
Image(systemName: "doc.text.magnifyingglass")
|
|
|
|
|
.font(.system(size: 46, weight: .regular))
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
.foregroundStyle(InterfacePalette.quietText(for: colorScheme))
|
|
|
|
|
.frame(width: 62, height: 62)
|
|
|
|
|
|
|
|
|
|
Text("Open a PDF")
|
|
|
|
|
.font(.title2.weight(.semibold))
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
appState.openDocument()
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Open PDF", systemImage: "doc")
|
|
|
|
|
}
|
|
|
|
|
.keyboardShortcut("o")
|
|
|
|
|
.keyboardShortcut(.defaultAction)
|
|
|
|
|
.controlSize(.large)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
RecentPDFsView()
|
|
|
|
|
.frame(maxWidth: 420)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 28)
|
|
|
|
|
.padding(.vertical, 36)
|
|
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
if isDropTargeted {
|
|
|
|
|
DropTargetOverlay()
|
|
|
|
|
.transition(.opacity.combined(with: .scale(scale: 0.985)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
|
|
|
.onDrop(
|
|
|
|
|
of: [UTType.fileURL.identifier],
|
|
|
|
|
isTargeted: $isDropTargeted
|
|
|
|
|
) { providers in
|
|
|
|
|
appState.openDroppedDocument(from: providers)
|
|
|
|
|
}
|
|
|
|
|
.animation(.easeInOut(duration: 0.16), value: isDropTargeted)
|
|
|
|
|
.onAppear {
|
|
|
|
|
appState.refreshRecentDocuments()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private struct RecentPDFsView: View {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
|
|
|
|
|
|
private var recentPDFs: [RecentDocumentItem] {
|
|
|
|
|
Array(appState.recentDocuments.prefix(5))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
if !recentPDFs.isEmpty {
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
HStack {
|
|
|
|
|
Text("Recent")
|
|
|
|
|
.font(.callout.weight(.semibold))
|
|
|
|
|
Spacer()
|
|
|
|
|
Button {
|
|
|
|
|
appState.clearRecentDocuments()
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Clear Recent PDFs", systemImage: "xmark.circle")
|
|
|
|
|
}
|
|
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
|
|
|
|
|
.help("Clear Recent PDFs")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ForEach(recentPDFs, id: \.id) { url in
|
|
|
|
|
Button {
|
|
|
|
|
appState.openRecentDocument(url.url)
|
|
|
|
|
} label: {
|
|
|
|
|
RecentPDFRow(item: url)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.help(url.url.path)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.padding(.top, 2)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct RecentPDFRow: View {
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
|
let item: RecentDocumentItem
|
|
|
|
|
|
|
|
|
|
private var detailText: String {
|
|
|
|
|
let pieces = [item.pageText, item.openedAt.map { Self.relativeDateFormatter.localizedString(for: $0, relativeTo: Date()) }]
|
|
|
|
|
.compactMap { $0 }
|
|
|
|
|
|
|
|
|
|
if pieces.isEmpty {
|
|
|
|
|
return item.folderName
|
|
|
|
|
}
|
|
|
|
|
return pieces.joined(separator: " - ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
HStack(spacing: 10) {
|
|
|
|
|
Image(systemName: "doc.text")
|
|
|
|
|
.font(.system(size: 16, weight: .regular))
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
.foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
|
|
|
|
|
.frame(width: 24, height: 24)
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text(item.title)
|
|
|
|
|
.font(.callout)
|
|
|
|
|
.foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
|
|
|
|
|
Text(detailText)
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
|
|
|
|
|
|
Image(systemName: "chevron.right")
|
|
|
|
|
.font(.caption.weight(.semibold))
|
|
|
|
|
.foregroundStyle(InterfacePalette.quietText(for: colorScheme))
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
.frame(height: 46)
|
|
|
|
|
.contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
|
|
|
|
.background {
|
|
|
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
|
|
.fill(InterfacePalette.subtleFill(for: colorScheme))
|
|
|
|
|
}
|
|
|
|
|
.overlay {
|
|
|
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
|
|
.stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static let relativeDateFormatter: RelativeDateTimeFormatter = {
|
|
|
|
|
let formatter = RelativeDateTimeFormatter()
|
|
|
|
|
formatter.unitsStyle = .abbreviated
|
|
|
|
|
formatter.dateTimeStyle = .named
|
|
|
|
|
return formatter
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct DropTargetOverlay: View {
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(spacing: 10) {
|
|
|
|
|
Image(systemName: "tray.and.arrow.down.fill")
|
|
|
|
|
.font(.system(size: 36, weight: .regular))
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
Text("Drop to Open")
|
|
|
|
|
.font(.title3.weight(.semibold))
|
|
|
|
|
}
|
|
|
|
|
.foregroundStyle(Color.accentColor)
|
2026-06-11 18:12:13 -07:00
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
2026-06-29 23:42:39 -07:00
|
|
|
.background(Color(nsColor: .windowBackgroundColor).opacity(colorScheme == .dark ? 0.72 : 0.82))
|
2026-06-24 17:51:26 -07:00
|
|
|
.overlay {
|
2026-06-29 23:42:39 -07:00
|
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
2026-06-24 17:51:26 -07:00
|
|
|
.stroke(
|
2026-06-29 23:42:39 -07:00
|
|
|
Color.accentColor.opacity(colorScheme == .dark ? 0.68 : 0.58),
|
2026-06-24 17:51:26 -07:00
|
|
|
style: StrokeStyle(lineWidth: 2, dash: [8, 6])
|
|
|
|
|
)
|
|
|
|
|
.padding(18)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct StatusBarView: View {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var annotationStatusText: String {
|
|
|
|
|
let count = appState.annotations.count
|
|
|
|
|
return count == 1 ? "1 annotation" : "\(count) annotations"
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:12:13 -07:00
|
|
|
var body: some View {
|
2026-06-29 23:42:39 -07:00
|
|
|
if appState.isCompactWindow {
|
|
|
|
|
compactStatus
|
|
|
|
|
} else {
|
|
|
|
|
regularStatus
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var regularStatus: some View {
|
2026-06-11 18:12:13 -07:00
|
|
|
HStack(spacing: 12) {
|
|
|
|
|
Text(appState.statusMessage)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
|
|
if appState.document != nil {
|
2026-06-24 17:51:26 -07:00
|
|
|
if appState.hasUnsentSidebarReplyDraft {
|
|
|
|
|
Text("Reply draft")
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
Text(annotationStatusText)
|
2026-06-11 18:12:13 -07:00
|
|
|
Text("Page \(appState.currentPageIndex + 1) of \(max(appState.pageCount, 1))")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.padding(.horizontal, 12)
|
|
|
|
|
.frame(height: 26)
|
|
|
|
|
.background(.bar)
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
|
|
|
|
|
private var compactStatus: some View {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Text(appState.statusMessage)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.middle)
|
|
|
|
|
|
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
|
|
|
|
|
|
if appState.document != nil {
|
|
|
|
|
if appState.hasUnsentSidebarReplyDraft {
|
|
|
|
|
Image(systemName: "text.bubble")
|
|
|
|
|
.help("Reply draft")
|
|
|
|
|
.accessibilityLabel("Reply draft")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text("\(appState.currentPageIndex + 1)/\(max(appState.pageCount, 1))")
|
|
|
|
|
.font(.caption.monospacedDigit())
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.fixedSize()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.font(.caption)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
.frame(height: 24)
|
|
|
|
|
.background(.bar)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ReaderToolbar: ToolbarContent {
|
|
|
|
|
@EnvironmentObject private var appState: AppState
|
2026-06-29 23:42:39 -07:00
|
|
|
@AppStorage(AppSettings.highlightColorStorageKey)
|
|
|
|
|
private var storedHighlightColor = AppSettings.defaultHighlightColorStorageValue
|
|
|
|
|
@State private var showsHighlightPalette = false
|
2026-06-11 18:12:13 -07:00
|
|
|
@FocusState private var searchFocused: Bool
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var activeHighlightColor: NSColor {
|
|
|
|
|
AppSettings.highlightColor(from: storedHighlightColor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var activeHighlightDisplayColor: Color {
|
|
|
|
|
Color(nsColor: AppSettings.displayColor(forHighlightColor: activeHighlightColor))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var pageNumberWidth: CGFloat {
|
|
|
|
|
CGFloat(max(2, String(max(appState.pageCount, 1)).count)) * (appState.isCompactWindow ? 8 : 8.4)
|
|
|
|
|
+ (appState.isCompactWindow ? 16 : 20)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var totalPageNumberWidth: CGFloat {
|
|
|
|
|
CGFloat(max(1, String(max(appState.pageCount, 1)).count)) * (appState.isCompactWindow ? 7.8 : 8.2)
|
|
|
|
|
+ (appState.isCompactWindow ? 18 : 21)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var pageSeparatorSpacing: CGFloat {
|
|
|
|
|
appState.isCompactWindow ? 28 : 46
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var pageControlWidth: CGFloat {
|
|
|
|
|
pageNumberWidth + totalPageNumberWidth + (appState.isCompactWindow ? 46 : 56)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var compactToolbarControlsEnabled: Bool {
|
|
|
|
|
appState.isCompactWindow
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var annotationToolsMenu: some View {
|
|
|
|
|
Menu {
|
2026-06-11 18:12:13 -07:00
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.toggleHighlighterMode()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Label(
|
|
|
|
|
appState.isHighlighterModeActive
|
|
|
|
|
? "Turn Highlighter Off (H)"
|
|
|
|
|
: "Turn Highlighter On (H)",
|
|
|
|
|
systemImage: "highlighter"
|
|
|
|
|
)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
.disabled(appState.document == nil)
|
2026-06-18 16:44:19 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.addUnderline()
|
2026-06-18 16:44:19 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Label("Underline Selection (U)", systemImage: "underline")
|
2026-06-18 16:44:19 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(appState.document == nil || !appState.hasTextSelection)
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.addComment()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Label("Comment (C)", systemImage: "text.bubble")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(appState.document == nil || !appState.hasTextSelection)
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "pencil.tip.crop.circle")
|
|
|
|
|
}
|
|
|
|
|
.help("Annotation Tools")
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
private var highlightColorButton: some View {
|
|
|
|
|
Button {
|
|
|
|
|
showsHighlightPalette.toggle()
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "circle.fill")
|
|
|
|
|
.foregroundStyle(activeHighlightDisplayColor)
|
|
|
|
|
}
|
|
|
|
|
.popover(isPresented: $showsHighlightPalette, arrowEdge: .bottom) {
|
|
|
|
|
HighlightPalettePopover(
|
|
|
|
|
storedHighlightColor: $storedHighlightColor,
|
|
|
|
|
onSelect: { color in
|
|
|
|
|
showsHighlightPalette = false
|
|
|
|
|
appState.selectHighlightColor(color, applyToSelection: appState.hasTextSelection)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.help("Highlight Color")
|
|
|
|
|
.accessibilityLabel("Highlight Color Palette")
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
var body: some ToolbarContent {
|
|
|
|
|
if appState.document == nil {
|
|
|
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
|
|
|
Button {
|
|
|
|
|
appState.openDocument()
|
|
|
|
|
} label: {
|
|
|
|
|
Label("Open", systemImage: "doc")
|
|
|
|
|
}
|
|
|
|
|
.help("Open PDF")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ToolbarItemGroup(placement: .navigation) {
|
|
|
|
|
Button {
|
|
|
|
|
appState.togglePageSidebar()
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "square.grid.2x2")
|
|
|
|
|
}
|
|
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
.help("Toggle Page Thumbnails")
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.toggleBookmarkForCurrentPage()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: appState.currentPageBookmark == nil ? "bookmark" : "bookmark.fill")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
.help(appState.bookmarkActionHelpText)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
ToolbarItem(placement: .principal) {
|
|
|
|
|
HStack(spacing: 5) {
|
2026-06-11 18:12:13 -07:00
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.goToPreviousPage()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "chevron.up")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(!appState.canGoToPreviousPage)
|
|
|
|
|
.help("Previous Page")
|
|
|
|
|
|
|
|
|
|
TextField("Page", text: $appState.pageText)
|
|
|
|
|
.textFieldStyle(.plain)
|
|
|
|
|
.multilineTextAlignment(.center)
|
|
|
|
|
.font(.system(size: 13, weight: .semibold, design: .rounded))
|
|
|
|
|
.frame(width: pageNumberWidth, height: compactToolbarControlsEnabled ? 21 : 22)
|
|
|
|
|
.onSubmit {
|
|
|
|
|
appState.goToPageFromField()
|
|
|
|
|
}
|
|
|
|
|
.disabled(appState.document == nil)
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
HStack(spacing: pageSeparatorSpacing) {
|
|
|
|
|
Text("/")
|
|
|
|
|
Text("\(max(appState.pageCount, 1))")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.font(.system(size: 13, weight: .regular, design: .rounded).monospacedDigit())
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.frame(width: totalPageNumberWidth + pageSeparatorSpacing, alignment: .leading)
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.goToNextPage()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "chevron.down")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.disabled(!appState.canGoToNextPage)
|
|
|
|
|
.help("Next Page")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.controlSize(.small)
|
|
|
|
|
.frame(width: pageControlWidth)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
|
|
|
if appState.showToolbarSearch {
|
|
|
|
|
HStack(spacing: 7) {
|
|
|
|
|
ZStack(alignment: .trailing) {
|
|
|
|
|
TextField("Search", text: $appState.searchText)
|
|
|
|
|
.textFieldStyle(.plain)
|
|
|
|
|
.font(.system(size: 13))
|
|
|
|
|
.padding(.leading, 10)
|
|
|
|
|
.padding(.trailing, appState.canClearSearchQuery ? 28 : 10)
|
|
|
|
|
.frame(height: compactToolbarControlsEnabled ? 26 : 28)
|
|
|
|
|
.background {
|
|
|
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
|
|
.fill(Color(nsColor: .textBackgroundColor))
|
|
|
|
|
}
|
|
|
|
|
.overlay {
|
|
|
|
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
|
|
|
|
.stroke(
|
|
|
|
|
searchFocused ? Color.accentColor : Color(nsColor: .separatorColor),
|
|
|
|
|
lineWidth: searchFocused ? 2 : 1
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.focused($searchFocused)
|
|
|
|
|
.onChange(of: appState.toolbarSearchFocusRequest) { _ in
|
|
|
|
|
searchFocused = true
|
|
|
|
|
}
|
|
|
|
|
.onSubmit {
|
|
|
|
|
appState.runSearch()
|
|
|
|
|
}
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
|
|
|
|
|
if appState.canClearSearchQuery {
|
|
|
|
|
Button {
|
|
|
|
|
appState.clearSearchQuery()
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "xmark.circle.fill")
|
|
|
|
|
.font(.system(size: 12, weight: .semibold))
|
|
|
|
|
.symbolRenderingMode(.hierarchical)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.padding(.trailing, 9)
|
|
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
.help("Clear Search")
|
|
|
|
|
.accessibilityLabel("Clear Search")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.frame(width: compactToolbarControlsEnabled ? 138 : 154, height: compactToolbarControlsEnabled ? 26 : 28)
|
|
|
|
|
|
|
|
|
|
if let searchSummaryText = appState.searchSummaryText {
|
|
|
|
|
Text(searchSummaryText)
|
|
|
|
|
.font(.system(size: 12, weight: .medium, design: .rounded).monospacedDigit())
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.frame(width: searchSummaryText == "No match" ? 58 : 34, alignment: .leading)
|
|
|
|
|
.layoutPriority(1)
|
|
|
|
|
.accessibilityLabel(searchSummaryText)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.previousSearchResult()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "chevron.left")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
|
.disabled(appState.searchResults.isEmpty)
|
|
|
|
|
.help("Previous Search Match")
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.nextSearchResult()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "chevron.right")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
|
.disabled(appState.searchResults.isEmpty)
|
|
|
|
|
.help("Next Search Match")
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.hideSearch()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "xmark")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
2026-06-11 18:12:13 -07:00
|
|
|
.disabled(appState.document == nil)
|
2026-06-29 23:42:39 -07:00
|
|
|
.help("Close Search")
|
|
|
|
|
} else {
|
2026-06-11 18:12:13 -07:00
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.showSearch()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "magnifyingglass")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
2026-06-11 18:12:13 -07:00
|
|
|
.disabled(appState.document == nil)
|
2026-06-29 23:42:39 -07:00
|
|
|
.help("Search")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ToolbarItem(placement: .primaryAction) {
|
|
|
|
|
annotationToolsMenu
|
|
|
|
|
}
|
2026-06-11 18:12:13 -07:00
|
|
|
|
2026-06-29 23:42:39 -07:00
|
|
|
ToolbarItem(placement: .primaryAction) {
|
|
|
|
|
highlightColorButton
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
2026-06-11 18:12:13 -07:00
|
|
|
Button {
|
|
|
|
|
appState.fitWidth()
|
|
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "arrow.left.and.right")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
2026-06-11 18:12:13 -07:00
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
.help("Fit to Width")
|
|
|
|
|
|
|
|
|
|
Button {
|
2026-06-29 23:42:39 -07:00
|
|
|
appState.toggleRightSidebarVisibility()
|
2026-06-11 18:12:13 -07:00
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "sidebar.right")
|
|
|
|
|
.foregroundStyle(appState.showCommentsSidebar ? Color.accentColor : Color.primary)
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
2026-06-11 18:12:13 -07:00
|
|
|
.disabled(appState.document == nil)
|
2026-06-29 23:42:39 -07:00
|
|
|
.help(appState.showCommentsSidebar ? "Hide Right Sidebar" : "Show Right Sidebar")
|
|
|
|
|
.accessibilityLabel("Toggle Right Sidebar")
|
2026-06-11 18:12:13 -07:00
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
appState.shareDocument()
|
|
|
|
|
} label: {
|
2026-06-29 23:42:39 -07:00
|
|
|
Image(systemName: "square.and.arrow.up")
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
.labelStyle(.iconOnly)
|
2026-06-11 18:12:13 -07:00
|
|
|
.disabled(appState.document == nil)
|
|
|
|
|
.help("Share PDF")
|
|
|
|
|
}
|
2026-06-29 23:42:39 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct HighlightPalettePopover: View {
|
|
|
|
|
@Binding var storedHighlightColor: String
|
|
|
|
|
let onSelect: (NSColor) -> Void
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
Image(systemName: "highlighter")
|
|
|
|
|
.foregroundStyle(Color(nsColor: AppSettings.displayColor(
|
|
|
|
|
forHighlightColor: AppSettings.highlightColor(from: storedHighlightColor)
|
|
|
|
|
)))
|
|
|
|
|
Text("Highlight")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
ForEach(Array(AppSettings.highlightSwatches.enumerated()), id: \.offset) { _, swatch in
|
|
|
|
|
Button {
|
|
|
|
|
storedHighlightColor = AppSettings.storageString(forHighlightColor: swatch.color)
|
|
|
|
|
onSelect(swatch.color)
|
|
|
|
|
} label: {
|
|
|
|
|
ZStack {
|
|
|
|
|
Circle()
|
|
|
|
|
.fill(Color(nsColor: AppSettings.displayColor(forHighlightColor: swatch.color)))
|
|
|
|
|
.frame(width: 24, height: 24)
|
|
|
|
|
.overlay {
|
|
|
|
|
Circle()
|
|
|
|
|
.stroke(
|
|
|
|
|
isSelected(swatch.color) ? Color.accentColor : Color(nsColor: .separatorColor),
|
|
|
|
|
lineWidth: isSelected(swatch.color) ? 2 : 0.8
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isSelected(swatch.color) {
|
|
|
|
|
Image(systemName: "checkmark")
|
|
|
|
|
.font(.system(size: 9, weight: .bold))
|
|
|
|
|
.foregroundStyle(Color(nsColor: .labelColor))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
.help(swatch.name)
|
|
|
|
|
.accessibilityLabel("Highlight \(swatch.name)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(14)
|
|
|
|
|
.frame(width: 284)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func isSelected(_ color: NSColor) -> Bool {
|
|
|
|
|
AppSettings.storageString(forHighlightColor: color) == storedHighlightColor
|
2026-06-11 18:12:13 -07:00
|
|
|
}
|
|
|
|
|
}
|