feat: add rendered notes view
This commit is contained in:
@@ -24,7 +24,6 @@ Todo:
|
|||||||
- Markdown formatting
|
- Markdown formatting
|
||||||
- Auto generate notes when recording is stopped
|
- Auto generate notes when recording is stopped
|
||||||
- Different note templates
|
- Different note templates
|
||||||
- Fix transcript UI alignment
|
|
||||||
|
|
||||||
Later:
|
Later:
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CC57181A2E201D8D0042BA88 /* notetaker */,
|
CC57181A2E201D8D0042BA88 /* notetaker */,
|
||||||
|
CC7816BF2E22093900101A90 /* Frameworks */,
|
||||||
CC5718192E201D8D0042BA88 /* Products */,
|
CC5718192E201D8D0042BA88 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -65,6 +66,13 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
CC7816BF2E22093900101A90 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -276,6 +284,7 @@
|
|||||||
ARCHS = "$(ARCHS_STANDARD)";
|
ARCHS = "$(ARCHS_STANDARD)";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -308,6 +317,7 @@
|
|||||||
ARCHS = "$(ARCHS_STANDARD)";
|
ARCHS = "$(ARCHS_STANDARD)";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = notetaker/notetaker.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ struct MeetingDetailView: View {
|
|||||||
@StateObject private var viewModel: MeetingViewModel
|
@StateObject private var viewModel: MeetingViewModel
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var showDeleteAlert = false
|
@State private var showDeleteAlert = false
|
||||||
|
@State private var isEditing = false
|
||||||
|
|
||||||
init(meeting: Meeting) {
|
init(meeting: Meeting) {
|
||||||
self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting))
|
self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting))
|
||||||
@@ -210,6 +211,7 @@ struct MeetingDetailView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.generateNotes()
|
await viewModel.generateNotes()
|
||||||
|
isEditing = false
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
@@ -225,14 +227,31 @@ struct MeetingDetailView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.disabled(viewModel.meeting.transcript.isEmpty)
|
.disabled(viewModel.meeting.transcript.isEmpty)
|
||||||
}
|
}
|
||||||
|
Button(action: {
|
||||||
|
isEditing.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: isEditing ? "pencil.circle.fill" : "pencil.circle")
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditor(text: $viewModel.meeting.generatedNotes)
|
if isEditing {
|
||||||
.font(.body)
|
TextEditor(text: Binding(
|
||||||
.scrollContentBackground(.hidden)
|
get: { viewModel.meeting.generatedNotes },
|
||||||
.background(Color.gray.opacity(0.05))
|
set: { viewModel.meeting.generatedNotes = $0 }
|
||||||
.cornerRadius(8)
|
))
|
||||||
.frame(maxHeight: .infinity)
|
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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..<dotIndex]), remaining[dotIndex..<remaining.endIndex].hasPrefix(". ") {
|
||||||
|
let contentStart = remaining.index(dotIndex, offsetBy: 2)
|
||||||
|
return (indentLevel, "\(num).", String(remaining[contentStart...]).trimmingCharacters(in: .whitespaces))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getBullet(for level: Int) -> 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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user