From ac7c2675d63f919dc22c032b1233a2be74a4937d Mon Sep 17 00:00:00 2001 From: Muhammed Ameen Date: Thu, 19 Mar 2026 08:07:17 +0530 Subject: [PATCH] Add pin entries to keep important entries at the top of sidebar Allows users to pin entries in the history sidebar so they stay above unpinned entries. Pin state persists across app launches via UserDefaults. Pinned entries show a small pin indicator, and a visual separator divides pinned from unpinned sections. --- freewrite/ContentView.swift | 81 ++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/freewrite/ContentView.swift b/freewrite/ContentView.swift index d099e14..bbacc4d 100644 --- a/freewrite/ContentView.swift +++ b/freewrite/ContentView.swift @@ -129,6 +129,8 @@ struct ContentView: View { @State private var selectedVideoHasTranscript = false @State private var backspaceDisabled = false // Add state for backspace toggle @State private var isHoveringBackspaceToggle = false // Add state for backspace toggle hover + @State private var pinnedEntryIds: Set = [] + @State private var hoveredPinId: UUID? = nil @State private var showingVideoRecording = false // Add state for video recording view @State private var isHoveringVideoButton = false // Add state for video button hover @State private var currentVideoURL: URL? = nil // Add state for current video being viewed @@ -867,7 +869,31 @@ struct ContentView: View { return colorScheme == .light ? Color.primary : Color.white } - + private var sortedEntries: [HumanEntry] { + let pinned = entries.filter { pinnedEntryIds.contains($0.id.uuidString) } + let unpinned = entries.filter { !pinnedEntryIds.contains($0.id.uuidString) } + return pinned + unpinned + } + + private func isEntryPinned(_ entry: HumanEntry) -> Bool { + pinnedEntryIds.contains(entry.id.uuidString) + } + + private func togglePin(_ entry: HumanEntry) { + let idString = entry.id.uuidString + if pinnedEntryIds.contains(idString) { + pinnedEntryIds.remove(idString) + } else { + pinnedEntryIds.insert(idString) + } + UserDefaults.standard.set(Array(pinnedEntryIds), forKey: "pinnedEntryIds") + } + + private func loadPinnedEntries() { + let saved = UserDefaults.standard.stringArray(forKey: "pinnedEntryIds") ?? [] + pinnedEntryIds = Set(saved) + } + var body: some View { let buttonBackground = colorScheme == .light ? Color.white : Color.black let navHeight: CGFloat = 68 @@ -1563,7 +1589,7 @@ struct ContentView: View { // Entries List ScrollView { LazyVStack(spacing: 0) { - ForEach(entries) { entry in + ForEach(sortedEntries) { entry in Button(action: { if selectedEntryId != entry.id { historyDebug("ROW TAP \(debugEntrySummary(entry))") @@ -1614,16 +1640,45 @@ struct ContentView: View { VStack(alignment: .leading, spacing: 4) { HStack { + if isEntryPinned(entry) && hoveredEntryId != entry.id { + Image(systemName: "pin.fill") + .font(.system(size: 9)) + .foregroundColor(.secondary) + .rotationEffect(.degrees(45)) + } Text(entry.previewText) .font(.system(size: 13)) .lineLimit(1) .foregroundColor(.primary) Spacer() - - // Export/Trash icons that appear on hover + + // Pin/Export/Trash icons that appear on hover if hoveredEntryId == entry.id { HStack(spacing: 8) { + // Pin button + Button(action: { + togglePin(entry) + }) { + Image(systemName: isEntryPinned(entry) ? "pin.slash" : "pin") + .font(.system(size: 11)) + .foregroundColor(hoveredPinId == entry.id ? + (colorScheme == .light ? .black : .white) : + (colorScheme == .light ? .gray : .gray.opacity(0.8))) + } + .buttonStyle(.plain) + .help(isEntryPinned(entry) ? "Unpin entry" : "Pin entry") + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.2)) { + hoveredPinId = hovering ? entry.id : nil + } + if hovering { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + // Export PDF button Button(action: { exportEntryAsPDF(entry: entry) @@ -1695,8 +1750,21 @@ struct ContentView: View { } .help("Click to select this entry") // Add tooltip - if entry.id != entries.last?.id { - Divider() + if entry.id != sortedEntries.last?.id { + // Thicker divider between pinned and unpinned sections + if isEntryPinned(entry) && !pinnedEntryIds.isEmpty { + let sortedList = sortedEntries + if let idx = sortedList.firstIndex(where: { $0.id == entry.id }), + idx + 1 < sortedList.count, + !isEntryPinned(sortedList[idx + 1]) { + Divider().frame(height: 2) + .background(Color.secondary.opacity(0.3)) + } else { + Divider() + } + } else { + Divider() + } } } } @@ -1729,6 +1797,7 @@ struct ContentView: View { .preferredColorScheme(colorScheme) .onAppear { showingSidebar = false // Hide sidebar by default + loadPinnedEntries() loadExistingEntries() } .onChange(of: showingVideoRecording) { _, isShowing in