Compare commits

...
3 Commits
Author SHA1 Message Date
james 97eb58d1dc feat: use a default template for automatic notes 2026-07-15 23:28:39 +02:00
james bcc16cd53f fix: stabilize template editor layout 2026-07-15 23:26:08 +02:00
james 25ae1ae710 ci: use current bundle preferences domain 2026-07-15 20:14:32 +02:00
12 changed files with 79 additions and 71 deletions
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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;
@@ -270,6 +270,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
/// - Returns: True if successful, false otherwise /// - Returns: True if successful, false otherwise
+1 -1
View File
@@ -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) {
+2 -4
View File
@@ -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,7 +68,7 @@ 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
@@ -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,9 +15,15 @@ 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) {
loadTemplates() loadTemplates()
+6
View File
@@ -145,6 +145,12 @@ struct SettingsView: View {
.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")
} label: { } label: {
+9 -27
View File
@@ -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)
+18 -1
View File
@@ -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)
@@ -39,6 +39,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) {
viewModel.deleteTemplate(template) viewModel.deleteTemplate(template)
@@ -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)