From 78393af91c9c119a5c5e68669d358a75ee28f428 Mon Sep 17 00:00:00 2001 From: Owen Gretzinger Date: Fri, 11 Jul 2025 23:16:32 -0400 Subject: [PATCH] feat: add rendered notes view --- README.md | 1 - notetaker.xcodeproj/project.pbxproj | 10 +++ notetaker/Views/MeetingDetailView.swift | 31 +++++++-- notetaker/Views/RenderedNotesView.swift | 87 +++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 notetaker/Views/RenderedNotesView.swift diff --git a/README.md b/README.md index 210bee1..396800c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ Todo: - Markdown formatting - Auto generate notes when recording is stopped - Different note templates -- Fix transcript UI alignment Later: diff --git a/notetaker.xcodeproj/project.pbxproj b/notetaker.xcodeproj/project.pbxproj index c7889cf..edd21a7 100644 --- a/notetaker.xcodeproj/project.pbxproj +++ b/notetaker.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ isa = PBXGroup; children = ( CC57181A2E201D8D0042BA88 /* notetaker */, + CC7816BF2E22093900101A90 /* Frameworks */, CC5718192E201D8D0042BA88 /* Products */, ); sourceTree = ""; @@ -65,6 +66,13 @@ name = Products; sourceTree = ""; }; + CC7816BF2E22093900101A90 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -276,6 +284,7 @@ ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -308,6 +317,7 @@ ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; diff --git a/notetaker/Views/MeetingDetailView.swift b/notetaker/Views/MeetingDetailView.swift index fb8d73f..ccd152e 100644 --- a/notetaker/Views/MeetingDetailView.swift +++ b/notetaker/Views/MeetingDetailView.swift @@ -32,6 +32,7 @@ struct MeetingDetailView: View { @StateObject private var viewModel: MeetingViewModel @Environment(\.dismiss) private var dismiss @State private var showDeleteAlert = false + @State private var isEditing = false init(meeting: Meeting) { self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting)) @@ -210,6 +211,7 @@ struct MeetingDetailView: View { Button(action: { Task { await viewModel.generateNotes() + isEditing = false } }) { HStack(spacing: 4) { @@ -225,14 +227,31 @@ struct MeetingDetailView: View { .buttonStyle(.plain) .disabled(viewModel.meeting.transcript.isEmpty) } + Button(action: { + isEditing.toggle() + }) { + Image(systemName: isEditing ? "pencil.circle.fill" : "pencil.circle") + } + .buttonStyle(.plain) } - TextEditor(text: $viewModel.meeting.generatedNotes) - .font(.body) - .scrollContentBackground(.hidden) - .background(Color.gray.opacity(0.05)) - .cornerRadius(8) - .frame(maxHeight: .infinity) + if isEditing { + TextEditor(text: Binding( + get: { viewModel.meeting.generatedNotes }, + set: { viewModel.meeting.generatedNotes = $0 } + )) + .font(.body) + .scrollContentBackground(.hidden) + .background(Color.gray.opacity(0.05)) + .cornerRadius(8) + .frame(maxHeight: .infinity) + } else { + RenderedNotesView(text: viewModel.meeting.generatedNotes) + .font(.body) + .background(Color.gray.opacity(0.05)) + .cornerRadius(8) + .frame(maxHeight: .infinity) + } } } } diff --git a/notetaker/Views/RenderedNotesView.swift b/notetaker/Views/RenderedNotesView.swift new file mode 100644 index 0000000..377a8b2 --- /dev/null +++ b/notetaker/Views/RenderedNotesView.swift @@ -0,0 +1,87 @@ +import SwiftUI + +struct RenderedNotesView: View { + let text: String + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 8) { + ForEach(text.split(separator: "\n"), id: \.self) { line in + self.renderLine(String(line)) + } + } + .padding() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + @ViewBuilder + private func renderLine(_ line: String) -> some View { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { + EmptyView() + } else if let level = headingLevel(for: trimmed) { + let content = String(trimmed.dropFirst(level + 1)).trimmingCharacters(in: .whitespaces) + Text(content) + .font(.system(size: CGFloat(28 - level * 4), weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + } else if let (indentLevel, bullet, content) = listItemInfo(for: line) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(bullet) + .foregroundColor(.gray) + Text(content) + .foregroundColor(.gray) + } + .padding(.leading, CGFloat(indentLevel * 15)) + } else { + Text(trimmed) + .foregroundColor(.primary) + } + } + + private func headingLevel(for line: String) -> Int? { + guard line.hasPrefix("#") else { return nil } + var count = 0 + for char in line { + if char == "#" { + count += 1 + } else if char == " " { + return count + } else { + return nil + } + } + return nil + } + + private func listItemInfo(for line: String) -> (indentLevel: Int, bullet: String, content: String)? { + let leadingSpaces = line.prefix(while: { $0 == " " }).count + let indentLevel = leadingSpaces / 2 + + let remaining = String(line.dropFirst(leadingSpaces)) + + if remaining.hasPrefix("- ") { + return (indentLevel, getBullet(for: indentLevel), String(remaining.dropFirst(2)).trimmingCharacters(in: .whitespaces)) + } else if remaining.hasPrefix("* ") { + return (indentLevel, getBullet(for: indentLevel), String(remaining.dropFirst(2)).trimmingCharacters(in: .whitespaces)) + } else if let dotIndex = remaining.firstIndex(of: "."), let num = Int(remaining[remaining.startIndex.. String { + switch level % 3 { + case 0: return "•" + case 1: return "◦" + case 2: return "▪︎" + default: return "-" + } + } +} + +#Preview { + RenderedNotesView(text: "# Heading 1\n## Heading 2\n- List item\n - Nested item\n - Deeper item\n1. Ordered\n 1. Nested ordered\nNormal text") +} \ No newline at end of file