Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97eb58d1dc | ||
|
|
bcc16cd53f | ||
|
|
25ae1ae710 |
@@ -35,8 +35,8 @@ jobs:
|
|||||||
| grep -q 'com.apple.security.network.server'
|
| grep -q 'com.apple.security.network.server'
|
||||||
- name: Smoke test local API
|
- name: Smoke test local API
|
||||||
run: |
|
run: |
|
||||||
defaults write owen.meetingnotes muteDeckAPIEnabled -bool true
|
defaults write net.jamesbone.meetingnotes muteDeckAPIEnabled -bool true
|
||||||
defaults write owen.meetingnotes muteDeckAPIPort -int 19880
|
defaults write net.jamesbone.meetingnotes muteDeckAPIPort -int 19880
|
||||||
"$RUNNER_TEMP/DerivedData/Build/Products/Release/Meetingnotes.app/Contents/MacOS/Meetingnotes" >"$RUNNER_TEMP/meetingnotes.log" 2>&1 &
|
"$RUNNER_TEMP/DerivedData/Build/Products/Release/Meetingnotes.app/Contents/MacOS/Meetingnotes" >"$RUNNER_TEMP/meetingnotes.log" 2>&1 &
|
||||||
app_pid=$!
|
app_pid=$!
|
||||||
trap 'kill "$app_pid" 2>/dev/null || true' EXIT
|
trap 'kill "$app_pid" 2>/dev/null || true' EXIT
|
||||||
|
|||||||
@@ -276,7 +276,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 15;
|
CURRENT_PROJECT_VERSION = 16;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.3;
|
MARKETING_VERSION = 1.1.4;
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 15;
|
CURRENT_PROJECT_VERSION = 16;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.3;
|
MARKETING_VERSION = 1.1.4;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
||||||
|
|||||||
@@ -269,6 +269,19 @@ class LocalStorageManager {
|
|||||||
|
|
||||||
return templates.sorted { $0.title < $1.title }
|
return templates.sorted { $0.title < $1.title }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func preferredTemplateID(in templates: [NoteTemplate]? = nil) -> UUID? {
|
||||||
|
let availableTemplates = templates ?? loadTemplates()
|
||||||
|
if let selectedTemplateID = UserDefaultsManager.shared.selectedTemplateId,
|
||||||
|
availableTemplates.contains(where: { $0.id == selectedTemplateID }) {
|
||||||
|
return selectedTemplateID
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackTemplateID = availableTemplates.first(where: { $0.title == "Standard Meeting" })?.id
|
||||||
|
?? availableTemplates.first?.id
|
||||||
|
UserDefaultsManager.shared.selectedTemplateId = fallbackTemplateID
|
||||||
|
return fallbackTemplateID
|
||||||
|
}
|
||||||
|
|
||||||
/// Deletes a template from local storage
|
/// Deletes a template from local storage
|
||||||
/// - Parameter template: The template to delete
|
/// - Parameter template: The template to delete
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ struct MeetingnotesApp: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.frame(minWidth: 700, minHeight: 400)
|
.frame(minWidth: 700, minHeight: 400)
|
||||||
}
|
}
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.automatic)
|
||||||
.defaultSize(width: 1000, height: 600)
|
.defaultSize(width: 1000, height: 600)
|
||||||
.commands {
|
.commands {
|
||||||
CommandGroup(after: .appInfo) {
|
CommandGroup(after: .appInfo) {
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ private final class LocalRecordingController {
|
|||||||
return statusPayload()
|
return statusPayload()
|
||||||
}
|
}
|
||||||
|
|
||||||
let meeting = Meeting()
|
let meeting = Meeting(templateId: LocalStorageManager.shared.preferredTemplateID())
|
||||||
guard LocalStorageManager.shared.saveMeeting(meeting) else {
|
guard LocalStorageManager.shared.saveMeeting(meeting) else {
|
||||||
throw LocalRecordingError.saveFailed
|
throw LocalRecordingError.saveFailed
|
||||||
}
|
}
|
||||||
@@ -288,9 +288,7 @@ private final class LocalRecordingController {
|
|||||||
meeting.transcriptChunks = chunks
|
meeting.transcriptChunks = chunks
|
||||||
let templates = LocalStorageManager.shared.loadTemplates()
|
let templates = LocalStorageManager.shared.loadTemplates()
|
||||||
if meeting.templateId == nil {
|
if meeting.templateId == nil {
|
||||||
meeting.templateId = UserDefaultsManager.shared.selectedTemplateId
|
meeting.templateId = LocalStorageManager.shared.preferredTemplateID(in: templates)
|
||||||
?? templates.first(where: { $0.title == "Standard Meeting" })?.id
|
|
||||||
?? templates.first?.id
|
|
||||||
}
|
}
|
||||||
_ = LocalStorageManager.shared.saveMeeting(meeting)
|
_ = LocalStorageManager.shared.saveMeeting(meeting)
|
||||||
NotificationCenter.default.post(name: .meetingSaved, object: meeting)
|
NotificationCenter.default.post(name: .meetingSaved, object: meeting)
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ class MeetingListViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createNewMeeting() -> Meeting {
|
func createNewMeeting() -> Meeting {
|
||||||
let newMeeting = Meeting()
|
let newMeeting = Meeting(templateId: LocalStorageManager.shared.preferredTemplateID())
|
||||||
meetings.insert(newMeeting, at: 0)
|
meetings.insert(newMeeting, at: 0)
|
||||||
_ = LocalStorageManager.shared.saveMeeting(newMeeting)
|
_ = LocalStorageManager.shared.saveMeeting(newMeeting)
|
||||||
// Track meeting creation event
|
// Track meeting creation event
|
||||||
PostHogSDK.shared.capture("meeting_created")
|
PostHogSDK.shared.capture("meeting_created")
|
||||||
return newMeeting
|
return newMeeting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,11 +228,13 @@ class MeetingViewModel: ObservableObject {
|
|||||||
func loadTemplates() {
|
func loadTemplates() {
|
||||||
templates = LocalStorageManager.shared.loadTemplates()
|
templates = LocalStorageManager.shared.loadTemplates()
|
||||||
|
|
||||||
// Load per-meeting template or default to Standard Meeting
|
// Keep an existing meeting's template, otherwise use the configured default.
|
||||||
if let meetingTemplateId = meeting.templateId {
|
if let meetingTemplateId = meeting.templateId,
|
||||||
|
templates.contains(where: { $0.id == meetingTemplateId }) {
|
||||||
selectedTemplateId = meetingTemplateId
|
selectedTemplateId = meetingTemplateId
|
||||||
} else if let defaultTemplate = templates.first(where: { $0.title == "Standard Meeting" }) {
|
} else {
|
||||||
selectedTemplateId = defaultTemplate.id
|
selectedTemplateId = LocalStorageManager.shared.preferredTemplateID(in: templates)
|
||||||
|
meeting.templateId = selectedTemplateId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,24 +52,7 @@ class SettingsViewModel: ObservableObject {
|
|||||||
|
|
||||||
func loadTemplates() {
|
func loadTemplates() {
|
||||||
templates = LocalStorageManager.shared.loadTemplates()
|
templates = LocalStorageManager.shared.loadTemplates()
|
||||||
|
settings.selectedTemplateId = LocalStorageManager.shared.preferredTemplateID(in: templates)
|
||||||
// Validate that the selected template still exists
|
|
||||||
if let selectedId = settings.selectedTemplateId {
|
|
||||||
if !templates.contains(where: { $0.id == selectedId }) {
|
|
||||||
// Selected template was deleted, clear the selection
|
|
||||||
settings.selectedTemplateId = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no template is selected, select the first default template
|
|
||||||
if settings.selectedTemplateId == nil {
|
|
||||||
if let defaultTemplate = templates.first(where: { $0.title == "Standard Meeting" }) {
|
|
||||||
settings.selectedTemplateId = defaultTemplate.id
|
|
||||||
} else if let firstTemplate = templates.first {
|
|
||||||
// Fallback to first available template
|
|
||||||
settings.selectedTemplateId = firstTemplate.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSettings(showMessage: Bool = true) {
|
func saveSettings(showMessage: Bool = true) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SwiftUI
|
|||||||
@MainActor
|
@MainActor
|
||||||
class TemplatesViewModel: ObservableObject {
|
class TemplatesViewModel: ObservableObject {
|
||||||
@Published var templates: [NoteTemplate] = []
|
@Published var templates: [NoteTemplate] = []
|
||||||
|
@Published private(set) var defaultTemplateID: UUID? = nil
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
|
|
||||||
@@ -14,8 +15,14 @@ class TemplatesViewModel: ObservableObject {
|
|||||||
func loadTemplates() {
|
func loadTemplates() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
templates = LocalStorageManager.shared.loadTemplates()
|
templates = LocalStorageManager.shared.loadTemplates()
|
||||||
|
defaultTemplateID = LocalStorageManager.shared.preferredTemplateID(in: templates)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDefaultTemplate(_ template: NoteTemplate) {
|
||||||
|
UserDefaultsManager.shared.selectedTemplateId = template.id
|
||||||
|
defaultTemplateID = template.id
|
||||||
|
}
|
||||||
|
|
||||||
func saveTemplate(_ template: NoteTemplate) {
|
func saveTemplate(_ template: NoteTemplate) {
|
||||||
if LocalStorageManager.shared.saveTemplate(template) {
|
if LocalStorageManager.shared.saveTemplate(template) {
|
||||||
@@ -42,4 +49,4 @@ class TemplatesViewModel: ObservableObject {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,12 @@ struct SettingsView: View {
|
|||||||
Text("Create and manage note templates")
|
Text("Create and manage note templates")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Picker("Default template", selection: $viewModel.settings.selectedTemplateId) {
|
||||||
|
ForEach(viewModel.templates) { template in
|
||||||
|
Text(template.title).tag(Optional(template.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
navigationPath.append("templates")
|
navigationPath.append("templates")
|
||||||
|
|||||||
@@ -33,16 +33,9 @@ struct TemplateEditView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
TextEditor(text: $template.context)
|
TextField("Meeting Context", text: $template.context, axis: .vertical)
|
||||||
.scrollContentBackground(.hidden)
|
.textFieldStyle(.roundedBorder)
|
||||||
.padding(8)
|
.lineLimit(4...10)
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.frame(minHeight: 100)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sections
|
// Sections
|
||||||
@@ -55,14 +48,12 @@ struct TemplateEditView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
template.sections.append(
|
||||||
template.sections.append(
|
TemplateSection(
|
||||||
TemplateSection(
|
title: "New Section",
|
||||||
title: "New Section",
|
description: "Description of this section"
|
||||||
description: "Description of this section"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
@@ -77,29 +68,20 @@ struct TemplateEditView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
ForEach(template.sections.indices, id: \.self) { index in
|
ForEach($template.sections) { $section in
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
TextField("Section Title", text: $template.sections[index].title)
|
TextField("Section Title", text: $section.title)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
TextEditor(text: $template.sections[index].description)
|
TextField("Section Description", text: $section.description, axis: .vertical)
|
||||||
.scrollContentBackground(.hidden)
|
.textFieldStyle(.roundedBorder)
|
||||||
.padding(8)
|
.lineLimit(3...8)
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.frame(minHeight: 60)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
template.sections.removeAll { $0.id == section.id }
|
||||||
let _ = template.sections.remove(at: index)
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "trash")
|
Image(systemName: "trash")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
@@ -152,4 +134,4 @@ struct TemplateEditView: View {
|
|||||||
)
|
)
|
||||||
) { _ in }
|
) { _ in }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct TemplateListView: View {
|
|||||||
Text(template.title)
|
Text(template.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
if template.isDefault {
|
if template.isDefault {
|
||||||
Text("Default")
|
Text("Built-in")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
@@ -38,6 +38,15 @@ struct TemplateListView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
viewModel.setDefaultTemplate(template)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: viewModel.defaultTemplateID == template.id ? "star.fill" : "star")
|
||||||
|
.foregroundColor(viewModel.defaultTemplateID == template.id ? .accentColor : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help(viewModel.defaultTemplateID == template.id ? "Default template" : "Use as default template")
|
||||||
|
|
||||||
if !template.isDefault {
|
if !template.isDefault {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
@@ -50,6 +59,14 @@ struct TemplateListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
|
if viewModel.defaultTemplateID != template.id {
|
||||||
|
Button {
|
||||||
|
viewModel.setDefaultTemplate(template)
|
||||||
|
} label: {
|
||||||
|
Label("Use as Default", systemImage: "star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !template.isDefault {
|
if !template.isDefault {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
viewModel.deleteTemplate(template)
|
viewModel.deleteTemplate(template)
|
||||||
@@ -94,4 +111,4 @@ struct TemplateListView: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
TemplateListView()
|
TemplateListView()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user